【问题标题】:Why does C++ multiplication with dynamic array work better than std::vector version为什么 C++ 与动态数组的乘法比 std::vector 版本更好
【发布时间】:2016-06-08 04:08:40
【问题描述】:

我正在为具有不同数据结构和技术(向量、数组和 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 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;

我正在查看一些以前的帖子,但我找不到任何与我的问题相关的内容linklink2link3

更新: 我用答案重构了测试,并且矢量效果更好:

向量乘法:时间: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 秒);

【问题讨论】:

  • 数据在内存中的排列方式不同。您的矩阵在内存中是连续的,而 vector&lt;vector&gt; 会分别分配每个向量。如果大小在编译时是固定的,您可以尝试vector&lt;array&lt;float,N&gt;&gt; 或执行其他操作以确保完整的矩阵在内存中是连续的。
  • 请参阅stackoverflow.com/questions/17259877/…,了解为什么您通常希望避免使用“真实”二维结构(如T**vector&lt;vector&lt;T&gt;&gt; ...)来存储密集矩阵。
  • 我猜内存布局不是你唯一的问题。向我们展示您的计时器代码以及您正在运行 openmp 版本的线程数。
  • @jepio 我正在逐步应用每个改进...我更改了分配错误,我检查了线程并发布了线程配置

标签: c++ arrays c++11 matrix openmp


【解决方案1】:

向量的向量类似于锯齿状数组——一个数组,其中每个条目都是一个指针,每个指针指向另一个浮点数组。

相比之下,原始数组版本是一块内存,您可以在其中进行数学运算以查找元素。

使用单个向量,而不是向量的向量,并手动计算。或者,使用固定大小的std::arrays 向量。或者编写一个辅助类型,它采用(一维)浮点向量,并为您提供它的二维视图。

连续缓冲区中的数据比一堆断开连接的缓冲区中的数据更易于缓存和优化。

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";
}

live example

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

【讨论】:

    【解决方案2】:

    你的方法完全不同:

    • 在“动态数组”版本中,您为每个矩阵分配一块内存,并将矩阵的行映射到该一维内存范围。

    • 在“向量”版本中,您使用“真实”和“动态”二维向量的向量,这意味着矩阵每一行的存储位置与其他行无关。

    你可能想做的是:

    • 使用vector&lt;float&gt;(size*size) 并手动执行与“动态数组”示例中相同的映射或

    • 编写一个在内部为您处理映射并提供二维访问接口的类(T&amp; operator()(size_t, size_t) 或某种row_proxy operator[](size_t),其中row_proxy 又具有T&amp; operator[](size_t)

    【讨论】:

      【解决方案3】:

      这只是为了强制执行有关连续内存的理论(在实践中)。

      在对使用 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]) 的额外内存访问,这些都会产生一些开销。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2014-01-11
        • 1970-01-01
        • 2014-10-24
        • 2012-11-25
        • 1970-01-01
        • 1970-01-01
        • 2020-04-27
        • 1970-01-01
        相关资源
        最近更新 更多