为什么C++;使用动态数组的乘法比std::vector版本更有效 我正在为不同的数据结构和技术(向量、数组和OpenMP)实现C++乘法,我发现了一个奇怪的情况…我的动态阵列版本工作得更好:
时代: openmp mult_1:时间:5.882000秒 阵列mult_2:时间:1.478000秒 我的编译标志是: /usr/bin/g++-fopenmp-pthread-std=c++1y-O3 C++矢量版本为什么C++;使用动态数组的乘法比std::vector版本更有效 我正在为不同的数据结构和技术(向量、数组和OpenMP)实现C++乘法,我发现了一个奇怪的情况…我的动态阵列版本工作得更好:,c++,arrays,c++11,matrix,openmp,C++,Arrays,C++11,Matrix,Openmp,时代: openmp mult_1:时间:5.882000秒 阵列mult_2:时间:1.478000秒 我的编译标志是: /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
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));
}
}
}
}
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 ms\n",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 ms\n",timer.now() / 1000);
delete [] p_matr1;
delete [] p_matr2;
delete [] p_result;
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));
}
}
}
}
我查看了以前的一些帖子,但找不到与我的问题相关的内容:
更新:
我用答案重构了测试,vector的效果稍微好一点:
矢量倍增:时间:1.194000秒
阵列mult_2:时间:1.202000秒
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));
}
}
}
}
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 ms\n",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 ms\n",timer.now() / 1000);
delete [] p_matr1;
delete [] p_matr2;
delete [] p_result;
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));
}
}
}
}
void mult(常量std::vector和matrixOne,常量std::vector和matrixTwo,std::vector和result,int size){
用于(int行=0;行<大小;++行){
用于(整数列=0;列 对于(int k=0;k而言,向量向量类似于锯齿数组——一个数组,其中每个条目都是指针,每个指针指向另一个浮点数组
相比之下,原始数组版本是一块内存,您可以在其中进行数学运算以查找元素
使用单个向量,而不是向量向量,然后手动进行计算。或者,使用固定大小的向量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
}
{}
};
模板
结构多尺寸数组视图辅助对象{
标准:尺寸常数*dims;
T*T;
std::size\u t stride()常量{
返回
multi_dim_array_view_helper{dims+1,nullptr}.stride()
**昏暗;
}
多维数组视图辅助运算符[](std::size\UTI)常量{
返回{dims+1,t+i*stride()};
}
};
模板
结构多尺寸数组视图辅助对象{
std::size\u t stride()常量{return 1;}
T*T;
T&operator[](标准::大小\u T i)常数{
返回t[i];
}
多维数组视图辅助对象(std::size\t const*,t*p):t(p){
};
模板
使用dims_t=std::array;
模板
结构多尺寸阵列视图存储
{
模糊存储;
};
模板
结构多维度数组视图:
多尺寸阵列视图存储,
多尺寸数组视图辅助对象
{
多尺寸阵列视图(尺寸d、t*t):
多维数组视图存储{std::move(d)},
多尺寸数组视图辅助对象{
这->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";
}
std::vector blah={
01.f、02.f、03.f、,
11.f、12.f、13.f、,
21.f、22.f、23.f、,
};
多维数组视图={3},blah.data()};
对于(标准::尺寸\u t i=0;i<3;++i)
{
std::cout您的方法非常不同:
- 在“动态数组”版本中,为每个矩阵分配一块内存,并将矩阵行映射到该一维内存范围
- 在“向量”版本中,您使用的向量是“实”和“动态”二维向量,这意味着矩阵每一行的存储位置与其他行无关
您可能想做的是:
- 使用
vector(size*size)
手动或手动执行与“dynamic array”示例中相同的映射
- 编写一个类,在内部为您处理映射,并提供一个二维访问接口(
T&operator()(size\T,size\T)
或某种row\u代理操作符[](size\T)
,其中row\u代理
依次具有T&operator[](size\T)
)
这只是为了强化关于连续内存的理论(在实践中)
对g++(-O2)生成的代码进行分析后,可以在以下位置找到源代码:
为阵列版本生成的相关代码为:
.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
毫不奇怪,编译器足够聪明,不会为所有的操作符[]
调用生成代码,并且很好地内联了它们,但是当它将值存储回结果向量时,它需要通过rdi+rcx
来跟踪不同的地址,以及对不同子向量的额外内存访问(mov-rsi,QWORD-PTR[rdx]
)所有这些都会产生一些开销。数据在内存中的排列方式不同。在执行向量时,矩阵在内存中是连续的。如果在编译时大小固定,则可以尝试向量
或执行其他操作,以确保完整的矩阵在内存中是连续的。请参阅关于您通常希望避免使用“真实”2d结构的原因(如T**
,vector
…)用于存储密集矩阵。我猜内存布局不是您唯一的问题。向我们展示您的计时器代码以及运行openmp版本的线程数。@jepio我正在一步一步地应用每个改进…我更改分配错误,检查线程并发布线程配置