为什么C++动态数组的乘法比 std::vector 版本更好
Why does C++ multiplication with dynamic array work better than std::vector version
我正在为具有不同数据结构和技术(向量、数组和 OpenMP)的矩阵实现C++乘法,我发现了一个奇怪的情况......我的动态阵列版本工作得更好:
次:
OpenMP mult_1: 时间: 5.882000 s
阵列mult_2: 时间: 1.478000 s
我的编译标志是:
/usr/bin/g++ -fopenmp -pthread -std=c++1y -O3
C++矢量版本
typedef std::vector<std::vector<float>> matrix_f;
void mult_1 (const matrix_f & matrixOne, const matrix_f & matrixTwo, matrix_f & result) {
const int matrixSize = (int)result.size();
#pragma omp parallel for simd
for (int rowResult = 0; rowResult < matrixSize; ++rowResult) {
for (int colResult = 0; colResult < matrixSize; ++colResult) {
for (int k = 0; k < matrixSize; ++k) {
result[rowResult][colResult] += matrixOne[rowResult][k] * matrixTwo[k][colResult];
}
}
}
}
动态阵列版本
void mult_2 ( float * matrixOne, float * matrixTwo, float * result, int size) {
for (int row = 0; row < size; ++row) {
for (int col = 0; col < size; ++col) {
for (int k = 0; k < size; ++k) {
(*(result+(size*row)+col)) += (*(matrixOne+(size*row)+k)) * (*(matrixTwo+(size*k)+col));
}
}
}
}
测试:
C++矢量版本
utils::ChronoTimer timer;
/* set Up simple matrix */
utils::matrix::matrix_f matr1 = std::vector<std::vector<float>>(size,std::vector<float>(size));
fillRandomMatrix(matr1);
utils::matrix::matrix_f matr2 = std::vector<std::vector<float>>(size,std::vector<float>(size));
fillRandomMatrix(matr2);
utils::matrix::matrix_f result = std::vector<std::vector<float>>(size,std::vector<float>(size));
timer.init();
utils::matrix::mult_1(matr1,matr2,result);
std::printf("openmp mult_1: time: %f msn",timer.now() / 1000);
动态阵列版本
utils::ChronoTimer timer;
float *p_matr1 = new float[size*size];
float *p_matr2 = new float[size*size];
float *p_result = new float[size*size];
fillRandomMatrixArray(p_matr1,size);
fillRandomMatrixArray(p_matr2,size);
timer.init();
utils::matrix::mult_2(p_matr1,p_matr2,p_result,size);
std::printf("array mult_2: time: %f msn",timer.now() / 1000);
delete [] p_matr1;
delete [] p_matr2;
delete [] p_result;
我正在检查以前的一些帖子,但我找不到任何与我的问题链接,链接2,链接3相关的帖子:
更新:我用答案重构了测试,矢量工作得稍微好一点:
矢量多:时间:1.194000 s
阵列mult_2:时间:1.202000 s
C++矢量版本
void mult (const std::vector<float> & matrixOne, const std::vector<float> & matrixTwo, std::vector<float> & result, int size) {
for (int row = 0; row < size; ++row) {
for (int col = 0; col < size; ++col) {
for (int k = 0; k <size; ++k) {
result[(size*row)+col] += matrixOne[(size*row)+k] * matrixTwo[(size*k)+col];
}
}
}
}
动态阵列版本
void mult_2 ( float * matrixOne, float * matrixTwo, float * result, int size) {
for (int row = 0; row < size; ++row) {
for (int col = 0; col < size; ++col) {
for (int k = 0; k < size; ++k) {
(*(result+(size*row)+col)) += (*(matrixOne+(size*row)+k)) * (*(matrixTwo+(size*k)+col));
}
}
}
}
此外,我的矢量化版本工作得更好(0.803 秒);
向量的向量类似于交错数组——一个数组,其中每个条目都是一个指针,每个指针指向另一个浮点数数组。
相比之下,原始数组版本是一个内存块,您可以在其中进行数学运算以查找元素。
使用单个向量,而不是向量的向量,并手动进行数学运算。 或者,使用固定大小的 std::array
s 向量。 或者编写一个帮助程序类型,该类型采用浮点数的(一维)向量,并为您提供它的二维视图。
比一堆断开连接的缓冲区中的数据更易于缓存和优化。
template<std::size_t Dim, class T>
struct multi_dim_array_view_helper {
std::size_t const* dims;
T* t;
std::size_t stride() const {
return
multi_dim_array_view_helper<Dim-1, T>{dims+1, nullptr}.stride()
* *dims;
}
multi_dim_array_view_helper<Dim-1, T> operator[](std::size_t i)const{
return {dims+1, t+i*stride()};
}
};
template<class T>
struct multi_dim_array_view_helper<1, T> {
std::size_t stride() const{ return 1; }
T* t;
T& operator[](std::size_t i)const{
return t[i];
}
multi_dim_array_view_helper( std::size_t const*, T* p ):t(p) {}
};
template<std::size_t Dims>
using dims_t = std::array<std::size_t, Dims-1>;
template<std::size_t Dims, class T>
struct multi_dim_array_view_storage
{
dims_t<Dims> storage;
};
template<std::size_t Dims, class T>
struct multi_dim_array_view:
multi_dim_array_view_storage<Dims, T>,
multi_dim_array_view_helper<Dims, T>
{
multi_dim_array_view( dims_t<Dims> d, T* t ):
multi_dim_array_view_storage<Dims, T>{std::move(d)},
multi_dim_array_view_helper<Dims, T>{
this->storage.data(), t
}
{}
};
现在您可以执行此操作:
std::vector<float> blah = {
01.f, 02.f, 03.f,
11.f, 12.f, 13.f,
21.f, 22.f, 23.f,
};
multi_dim_array_view<2, float> view = { {3}, blah.data() };
for (std::size_t i = 0; i < 3; ++i )
{
std::cout << "[";
for (std::size_t j = 0; j < 3; ++j )
std::cout << view[i][j] << ",";
std::cout << "]n";
}
现场示例
视图类中不复制任何数据。 它只提供平面数组的视图,该数组是一个多维数组。
您的方法完全不同:
-
在"动态数组"版本中,您可以为每个矩阵分配一个内存块,并将矩阵的行映射到该一维内存范围。
在"向量" 版本中,您使用"真实"和"动态"二维向量的向量,这意味着矩阵的每一行的存储位置相对于其他行无关。
您可能想做的是:
-
使用
vector<float>(size*size)
并手动执行您在"动态数组"示例中执行的相同映射,或者 -
编写一个类,该类在内部为您处理映射并提供二维访问接口(
T& operator()(size_t, size_t)
或某种row_proxy operator[](size_t)
,其中row_proxy
又具有T& operator[](size_t)
)
这只是为了强化(在实践中)关于连续记忆的理论。
在对使用 g++ (-O2) 生成的代码进行一些分析后,可以在以下位置找到源代码: https://gist.github.com/42be237af8e3e2b1ca03
为数组版本生成的相关代码为:
.L3:
lea r9, [r13+0+rbx] ; <-------- KEEPS THE ADDRESS
lea r11, [r12+rbx]
xor edx, edx
.L7:
lea r8, [rsi+rdx]
movss xmm1, DWORD PTR [r9]
xor eax, eax
.L6:
movss xmm0, DWORD PTR [r11+rax*4]
add rax, 1
mulss xmm0, DWORD PTR [r8]
add r8, r10
cmp ecx, eax
addss xmm1, xmm0
movss DWORD PTR [r9], xmm1 ; <------------ ADDRESS IS USED
jg .L6
add rdx, 4
add r9, 4 ; <--- ADDRESS INCREMENTED WITH SIZE OF FLOAT
cmp rdx, rdi
jne .L7
add ebp, 1
add rbx, r10
cmp ebp, ecx
jne .L3
了解 r9
值的用法如何反映目标数组的连续内存和其中一个输入数组的r8
。
另一方面,向量的向量生成如下代码:
.L12:
mov r9, QWORD PTR [r12+r11]
mov rdi, QWORD PTR [rbx+r11]
xor ecx, ecx
.L16:
movss xmm1, DWORD PTR [rdi+rcx]
mov rdx, r10
xor eax, eax
jmp .L15
.L13:
movaps xmm1, xmm0
.L15:
mov rsi, QWORD PTR [rdx]
movss xmm0, DWORD PTR [r9+rax]
add rax, 4
add rdx, 24
cmp r8, rax
mulss xmm0, DWORD PTR [rsi+rcx]
addss xmm0, xmm1
movss DWORD PTR [rdi+rcx], xmm0 ; <------------ HERE
jne .L13
add rcx, 4
cmp rcx, r8
jne .L16
add r11, 24
cmp r11, rbp
jne .L12
毫不奇怪,编译器足够聪明,不会为所有operator []
调用生成代码,并且在内联它们方面做得很好,但是看看当它将值存储回结果向量时,它需要如何通过rdi + rcx
跟踪不同的地址,以及各种子向量(mov rsi, QWORD PTR [rdx]
)的额外内存访问,这些都会产生一些开销。
- 使用std::vector的OpenCL矩阵乘法
- POCO::PostgreSQL:如何将std::vector支持添加到`Binder::bind`
- std::vector的包装器,使数组的结构看起来像结构的数组
- 编译器如何区分std::vector的构造函数
- 使用 pqxx 将 std::vector 存储在 postgresql 中,并从数据库中检索它
- 在std::vector上存储带有模板的类实例
- 在main()之外初始化std::vector会导致性能下降(多线程)
- 为什么std::vector比数组慢
- std::vector::迭代器是否可以合法地作为指针
- 如何将二进制格式的 C++ 对象的 std::vector 保存到磁盘?
- 为什么std::vector和std::valarray初始化构造函数不同
- ";结果类型必须是可从输入范围的值类型""构造的;创建std::vector时
- 在没有未定义行为的情况下实现类似std::vector的容器
- 如何调整 std::vector of Eigen::MatrixXd 的大小
- 使用 std::vector::reverse_iterator 将 int 序列化为字节向量?
- 如何将AERT_Allocate与 std:vector 一起使用
- 推导 std::vector::back() 的返回类型
- 如何将原始字节附加到 std::vector?
- std::vector 没有重载函数的实例与参数列表匹配
- 如果 KEY 是 std::list 或 std::vector 而不是值,那么 std::map 的默认行为是什么?