首页
学习
活动
专区
圈层
工具
发布
社区首页 >问答首页 >为什么动态数组的C++乘法比std::向量版本工作得更好

为什么动态数组的C++乘法比std::向量版本工作得更好
EN

Stack Overflow用户
提问于 2016-02-25 11:46:10
回答 3查看 1.3K关注 0票数 9

我正在用不同的数据结构和技术(向量、数组和OpenMP)实现矩阵的C++乘法,我发现了一个奇怪的情况.我的动态数组版本运行得更好:

时代:

openmp mult_1:时间: 5.882000 s 数组mult_2:时间: 1.478000 s

我的编译标志是:

/usr/bin/g++ -fopenmp -pthread -std=c++1y -O3

C++矢量版本

代码语言:javascript
复制
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];  
            }
        }
    }
}

动态数组版本

代码语言:javascript
复制
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++矢量版本

代码语言:javascript
复制
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);

动态数组版本

代码语言:javascript
复制
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;

我正在检查以前的一些帖子,但是我找不到与我的问题链接link2link3有关的任何相关信息。

更新:I用答案对测试进行重构,向量更轻巧:

向量机:时间: 1.194000秒 数组mult_2:时间: 1.202000 s

C++矢量版本

代码语言:javascript
复制
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];
            }
        }
    }
}

动态数组版本

代码语言:javascript
复制
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 s);

EN

回答 3

Stack Overflow用户

回答已采纳

发布于 2016-02-25 12:11:04

向量向量类似于交错数组--一个数组,其中每个条目都是一个指针,每个指针指向另一个浮动数组。

相比之下,原始数组版本是一个内存块,您可以通过数学来查找元素。

使用单个向量,而不是向量向量,手工计算。或者,使用一个固定大小的std::array向量,或者编写一个使用浮点(一维)向量的助手类型,并给出它的二维视图。

与一组断开的缓冲区中的数据相比,连续缓冲区中的数据更容易缓存和优化。

代码语言:javascript
复制
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
    }
  {}
};

现在你可以这样做了:

代码语言:javascript
复制
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";
}

实例化

视图类中没有复制数据。它只是提供了平面数组的视图,这是一个多维数组.

票数 12
EN

Stack Overflow用户

发布于 2016-02-25 12:22:44

您的方法完全不同:

  • 在“动态数组”版本中,为每个矩阵分配一块内存,并将矩阵的行映射到该一维内存范围。
  • 在“向量”版本中,使用的向量是“真实的”和“动态的”二维向量,这意味着矩阵中每一行的存储位置与其他行无关。

你可能想做的是:

  • 使用vector<float>(size*size)并执行您在“动态数组”示例中手工执行的完全相同的映射
  • 编写一个类,在内部为您处理映射,并提供一个二维访问接口(T& operator()(size_t, size_t)或某种row_proxy operator[](size_t),其中row_proxy又有T& operator[](size_t))。
票数 2
EN

Stack Overflow用户

发布于 2016-02-25 12:33:48

这只是为了加强(在实践中)关于连续记忆的理论。

在对用g++ (-O2)生成的代码做了一些分析之后,可以在:https://gist.github.com/42be237af8e3e2b1ca03找到源代码

为数组版本生成的相关代码是:

代码语言:javascript
复制
.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

在另一端,向量向量生成代码如下:

代码语言:javascript
复制
.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])的额外内存访问,这些都会产生一些开销。

票数 1
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/35626375

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档