c++ - 矩阵乘法、KIJ 顺序、并行版本比非并行版本慢

标签 c++ c matrix openmp

我有一项关于并行编程的学校任务,我遇到了很多问题。 我的任务是创建给定矩阵乘法代码的并行版本并测试其性能(是的,它必须按 KIJ 顺序):

void multiply_matrices_KIJ()
{
    for (int k = 0; k < SIZE; k++)
        for (int i = 0; i < SIZE; i++)
            for (int j = 0; j < SIZE; j++)
                matrix_r[i][j] += matrix_a[i][k] * matrix_b[k][j];
}

这是我到目前为止想出的:

void multiply_matrices_KIJ()
{
    for (int k = 0; k < SIZE; k++)
#pragma omp parallel
    {
#pragma omp for schedule(static, 16)
        for (int i = 0; i < SIZE; i++)
            for (int j = 0; j < SIZE; j++)
                matrix_r[i][j] += matrix_a[i][k] * matrix_b[k][j];
    }
}

这就是我发现让我感到困惑的地方。这个代码的并行版本比非并行版本慢 50% 左右。根据矩阵大小,速度差异仅略有不同(测试的 SIZE = 128、256、512、1024、2048 和各种计划版本 - 动态、静态、完全没有它等等)。

有人可以帮助我了解我做错了什么吗?是否可能是因为我使用的是 KIJ 命令,使用 openMP 不会变得更快?

编辑:

我在 Windows 7 PC 上工作,使用 Visual Studio 2015 Community Edition,在 Release x86 模式下编译(x64 也无济于事)。我的 CPU 是:Intel Core i5-2520M CPU @ 2,50GHZ(是的,是的,这是一台笔记本电脑,但我在家用 I7 PC 上得到了相同的结果)

我正在使用全局数组:

float matrix_a[SIZE][SIZE];    
float matrix_b[SIZE][SIZE];    
float matrix_r[SIZE][SIZE];

我正在为矩阵 a 和 b 分配随机(浮点)值,矩阵 r 填充为 0。

到目前为止,我已经用各种矩阵大小(128、256、512、1024、2048 等)测试了代码。对于其中一些,它的目的是不适合缓存。 我当前的代码版本如下所示:

void multiply_matrices_KIJ()
{
#pragma omp parallel 
    {
    for (int k = 0; k < SIZE; k++) {
#pragma omp for schedule(dynamic, 16) nowait
        for (int i = 0; i < SIZE; i++) {
            for (int j = 0; j < SIZE; j++) {
                matrix_r[i][j] += matrix_a[i][k] * matrix_b[k][j];
            }
        }
    }
    }
}

需要说明的是,我知道通过不同的循环顺序我可以获得更好的结果,但事实就是如此——我必须使用 KIJ 顺序。我的任务是并行执行 KIJ for 循环并检查性能提升情况。我的问题是我希望(ed)至少执行得更快一点(比我现在得到的最多快 5-10%)即使它是并行的 I 循环(不能用K 循环,因为我会得到不正确的结果,因为它是 matrix_r[i][j])。

这些是我在使用上面显示的代码时得到的结果(我正在计算数百次并获得平均时间):

大小 = 128

  • 序列号:0,000608s
  • 并行 I,计划(动态,16):0,000683s
  • 并行 I,计划(静态,16):0,000647s
  • 平行 J,无时间表:0,001978 秒(这是我执行的位置 执行速度较慢)

大小 = 256

  • 序列号:0,005787s
  • 并行 I,计划(动态,16):0,005125s
  • 并行 I,计划(静态,16):0,004938s
  • 并行 J,无调度:0,013916s

大小 = 1024

  • 序列号:0,930250s
  • 并行 I,计划(动态,16):0,865750s
  • 并行 I,计划(静态,16):0,823750s
  • 平行 J,无时间表:1,137000s

最佳答案

注意:此答案与如何从循环顺序中获得最佳性能或如何并行化它无关,因为出于多种原因我认为它不是最优的。我将尝试就如何改进顺序(并将其并行化)提供一些建议。

循环顺序

OpenMP 通常用于在多个 CPU 上分配工作。因此,您希望最大化每个线程的工作负载,同时最小化所需的数据和信息传输量。

  1. 您想并行执行最外层循环而不是第二个循环。因此,您需要将 r_matrix 索引之一作为外循环索引,以避免在写入结果矩阵时出现竞争条件。

  2. 接下来您要按内存存储顺序遍历矩阵(将变化较快的索引作为第二个而不是第一个下标索引)。

您可以通过以下循环/索引顺序实现两者:

for i = 0 to a_rows
  for k = 0 to a_cols
    for j = 0 to b_cols
      r[i][j] = a[i][k]*b[k][j]

在哪里

  • jik 变化快,ki 变化快>.
  • i是结果矩阵下标,i循环可以并行运行

以这种方式重新排列您的 multiply_matrices_KIJ 已经提供了相当多的性能提升。

我做了一些简短的测试,我用来比较时间的代码是:

template<class T>
void mm_kij(T const * const matrix_a, std::size_t const a_rows, 
  std::size_t const a_cols, T const * const matrix_b, std::size_t const b_rows, 
  std::size_t const b_cols, T * const matrix_r)
{
  for (std::size_t k = 0; k < a_cols; k++)
  {
    for (std::size_t i = 0; i < a_rows; i++)
    {
      for (std::size_t j = 0; j < b_cols; j++)
      {
        matrix_r[i*b_cols + j] += 
          matrix_a[i*a_cols + k] * matrix_b[k*b_cols + j];
      }
    }
  }
}

模仿您的multiply_matrices_KIJ() 函数对比

template<class T>
void mm_opt(T const * const a_matrix, std::size_t const a_rows, 
  std::size_t const a_cols, T const * const b_matrix, std::size_t const b_rows, 
  std::size_t const b_cols, T * const r_matrix)
{
  for (std::size_t i = 0; i < a_rows; ++i)
  { 
    T * const r_row_p = r_matrix + i*b_cols;
    for (std::size_t k = 0; k < a_cols; ++k)
    { 
      auto const a_val = a_matrix[i*a_cols + k];
      T const * const b_row_p = b_matrix + k * b_cols;
      for (std::size_t j = 0; j < b_cols; ++j)
      { 
        r_row_p[j] += a_val * b_row_p[j];
      }
    }
  }
}

执行上述命令。

Time consumption for multiplication of two 2048x2048 matrices on Intel i5-2500k

  • mm_kij(): 6.16706s.

  • mm_opt(): 2.6567s.

给定的顺序还允许外循环并行化,而不会在写入结果矩阵时引入任何竞争条件:

template<class T>
void mm_opt_par(T const * const a_matrix, std::size_t const a_rows, 
  std::size_t const a_cols, T const * const b_matrix, std::size_t const b_rows, 
  std::size_t const b_cols, T * const r_matrix)
{
#if defined(_OPENMP)
  #pragma omp parallel
  {
    auto ar = static_cast<std::ptrdiff_t>(a_rows);
    #pragma omp for schedule(static) nowait
    for (std::ptrdiff_t i = 0; i < ar; ++i)
#else
    for (std::size_t i = 0; i < a_rows; ++i)
#endif
    {
      T * const r_row_p = r_matrix + i*b_cols;
      for (std::size_t k = 0; k < b_rows; ++k)
      {
        auto const a_val = a_matrix[i*a_cols + k];
        T const * const b_row_p = b_matrix + k * b_cols;
        for (std::size_t j = 0; j < b_cols; ++j)
        {
          r_row_p[j] += a_val * b_row_p[j];
        }
      }
    }
#if defined(_OPENMP)
  }
#endif
}

每个线程写入一个单独的结果行

Time consumption for multiplication of two 2048x2048 matrices on Intel i5-2500k (4 OMP threads)

  • mm_kij(): 6.16706s.

  • mm_opt(): 2.6567s.

  • mm_opt_par(): 0.968325s.

不是完美的缩放,但作为一个比串行代码更快的开始。

关于c++ - 矩阵乘法、KIJ 顺序、并行版本比非并行版本慢,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/35631216/

相关文章:

C++ WINAPI 在挂载的 VHD 上创建多个分区

R——比较计算的相关性

r - data.frame 中所有变量对的元素乘法总和

c++ - 如何优化一个周期?

c++ - 为什么派生类可以调用基类的成员函数?

c - 字符常量中的多个字符

c - Windows Phone 和 ANSI C(非 C++)库

c++ - 如何判断多维数组中的元素是否为空

python - Boost.Python 向现有 PyObject 添加绑定(bind)(用于异常处理)

c - 我是否正确地使用 OpenSSL 对缓冲区进行了 base64 编码?