矩阵乘法,KIJ阶,并行版慢于非平行

时间:2016-02-25 15:14:31

标签: c++ c matrix openmp

我有一个关于paralel编程的学校任务,我遇到了很多问题。 我的任务是创建给定矩阵乘法代码的并行版本并测试其性能(是的,它必须以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和各种计划版本 - 动态,静态,到目前为止还没有它等。) / p>

有人可以帮我理解我做错了什么吗?可能是因为我使用了KIJ订单而且使用openMP它不会更快?

编辑:

我正在使用Visual Studio 2015社区版在Windows 7 PC上工作,在Release x86模式下进行编译(x64也没有帮助)。我的CPU是:英特尔酷睿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循环并检查性能的提高。我的问题是,我希望(编辑)至少快一点的执行速度(比我现在获得的速度快多了5-10%),即使它是并行的I循环(可以&# 39;用K循环来做,因为我会得到不正确的结果,因为它是矩阵_r [i] [j])。

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

SIZE = 128

  • 串行版本:0,000608s
  • Parallel I,schedule(dynamic,16):0,000683s
  • Parallel I,schedule(static,16):0,000647s
  • 并行J,没有时间表:0,001978s(这是我执行的地方 执行速度较慢)

SIZE = 256

  • 系列版本:0,005787s
  • Parallel I,schedule(dynamic,16):0,005125s
  • Parallel I,schedule(static,16):0,004938s
  • 并行J,没有时间表:0,013916s

SIZE = 1024

  • 串行版本:0,930250s
  • Parallel I,schedule(dynamic,16):0,865750s
  • Parallel I,schedule(static,16):0,823750s
  • 并行J,没有时间表:1,137000s

3 个答案:

答案 0 :(得分:4)

注意:这个答案不是关于如何从循环次序中获得最佳性能或如何并行化它,因为我认为由于几个原因它是次优的。我会尝试就如何改进订单(并将其并行化)提出一些建议。

循环顺序

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

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

  2. 接下来就是你想要以内存存储顺序遍历矩阵(具有更快的变化索引作为第二个而不是第一个下标索引)。

  3. 您可以使用以下循环/索引顺序来实现两者:

    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]
    

    哪里

    • j的变化速度超过ikk的变化速度超过i
    • 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];
          }
        }
      }
    }
    

    实施上述订单。

      

    英特尔i5-2500k上两个 2048x2048 矩阵相乘的时间消耗

         
        
    • 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
    }
    

    每个线程写入单个结果行的位置

      

    英特尔i5-2500k(4个OMP线程)上两个 2048x2048 矩阵相乘的时间消耗

         
        
    • mm_kij():6.16706s。

    •   
    • mm_opt():2.6567s。

    •   
    • mm_opt_par():0.968325s。

    •   

    不完美缩放,但比串行代码更快。

答案 1 :(得分:1)

OpenMP实现创建了一个线程池(尽管OpenMP标准没有规定线程池,我已经看到过OpenMP的每个实现都这样做),因此每次并行区域都不必创建和销毁线程进入。然而,每个并行区域之间存在障碍,因此所有线程都必须同步。并行区域之间的fork连接模型可能存在一些额外的开销。因此,即使不必重新创建线程,它们仍然必须在并行区域之间进行初始化。可以找到更多详细信息here

为了避免进入并行区域之间的开销,我建议在最外层循环上创建并行区域,但在i上的内循环上进行工作共享,如下所示:

void multiply_matrices_KIJ() {
    #pragma omp parallel
    for (int k = 0; k < SIZE; k++)
        #pragma omp for schedule(static) 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];
}

使用#pragma omp for时存在隐含障碍。 nowait子句删除了障碍。

还要确保使用优化进行编译。在未启用优化的情况下比较性能几乎没有意义。我会使用-O3

答案 2 :(得分:0)

请始终牢记,对于缓存目的,循环的最佳排序将是最慢的 - &gt;最快的。在你的情况下,这意味着我,K,L顺序。如果您的序列代码没有被编译器从KIJ-&gt; IKL排序中自动重新排序(假设您有“-O3”),我会感到非常惊讶。但是,编译器无法使用并行循环执行此操作,因为这会破坏您在并行区域中声明的逻辑。

如果你真的无法对你的循环重新排序,那么你最好的选择可能是重写并行区域以包含最大可能的循环。如果您有OpenMP 4.0,您也可以考虑在最快的维度上使用SIMD矢量化。但是,由于KIJ订购中固有的上述缓存问题,我仍然怀疑你能够打败你的串行代码......

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