当前位置: 首页 > 知识库问答 >
问题:

矩阵乘法,KIJ阶,并行版本比非并行版本慢

郗缪文
2023-03-14

我有一个关于平行编程的学校任务,我遇到了很多问题。我的任务是创建给定矩阵乘法代码的并行版本并测试其性能(是的,它必须按 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社区版,在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,并检查性能的提高。我的问题是我期望至少有一点点更快的执行速度(比我现在得到的最多快5-10%),即使它是并行的I循环(不能用K循环,因为它是matrix_r[i][j])。

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

  • 序列版:0,000608s
  • 并行I,时间表(动态,16):0,000683s
  • 并行I,时间表(静态,16):0,000647s
  • 并行J,没有计划:0,001978s(这是我遇到执行速度较慢的地方)
  • 串行版本: 0,005787s
  • 并行 I,计划(动态,16):0,005125s
  • 并行 I,时间表(静态,16):0,004938s
  • 并行J,无时间表:0,013916s
  • 串行版本: 0,930250s
  • 并行 I,时间表(动态,16):0,865750s
  • 并行 I,时间表(静态,16):0,823750s
  • 并行J,无时间表:1,137000s

共有3个答案

毋琪
2023-03-14

永远记住,出于缓存目的,循环的最佳排序将是最慢的 -

如果你真的不能重新排序你的循环,那么你最好的选择可能是重写并行区域,以包含尽可能大的循环。如果你有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];
    }
}
周翼
2023-03-14

OpenMP实现创建了一个线程池(尽管OpenMP标准没有强制要求线程池,但我见过的每个OpenMP实现都是这样做的),这样就不必在每次进入并行区域时都创建和销毁线程。然而,每个并行区域之间有一个屏障,因此所有线程必须同步。在并行区域之间的fork join模型中可能会有一些额外的开销。因此,即使不需要重新创建线程,它们仍然需要在并行区域之间进行初始化。更多细节可以在这里找到。

为了避免进入并行区域之间的开销,我建议在最外层的循环上创建并行区域,但在内部循环上进行工作共享,我喜欢这样:

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

使用< code>#pragma omp for时存在一个隐含的障碍。< code>nowait子句移除该障碍。

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

沈建柏
2023-03-14

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

循环顺序

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

>

  • 您希望并行执行最外层的循环,而不是第二个循环。因此,您需要将其中一个r_matrix索引作为外部循环索引,以避免在写入结果矩阵时出现争用条件。

    接下来的事情是,您希望按照内存存储顺序遍历矩阵(将变化更快的索引作为第二个而不是第一个下标索引)。

    您可以使用以下循环/索引顺序实现这两个目标:

    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]
    

    在哪里

      < li> j比< code>i或< code>k变化快,< code>k比< code>i变化快。 < li> i是结果矩阵下标,并且< code>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];
          }
        }
      }
    }
    

    执行上述命令。

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

    >

  • mm_kij(): 6.16706s。

    mm_opt():2.6567秒。

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

    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 上乘法两个 2048x2048 矩阵(4 个 OMP 线程)所消耗的时间

    >

  • mm_kij(): 6.16706s。

    mm_opt():2.6567秒。

    < code>mm_opt_par(),0.968325s

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

  •  类似资料:
    • 问题内容: 昨天我问一个问题关于并行矩阵乘法Java 7中使用fork /join框架这里。在axtavt的帮助下,我的示例程序开始工作。现在,我仅使用Java 6功能来实现等效程序。我遇到了与昨天相同的问题,尽管应用了axtavt给我的反馈(我认为)。我在俯视什么吗?码: 问题答案: 阅读了这个问题后,我决定改编我的程序。我的新程序无需同步即可运行良好。谢谢您的想法,彼得。 新代码:

    • 我正在计算两大组向量(具有相同特征)之间的余弦相似度。每组向量表示为一个scipy CSR稀疏矩阵a和B。我想计算一个x B^T,它不会稀疏。但是,我只需要跟踪超过某个阈值的值,例如0.8。我正试图用vanilla RDD在Pyspark中实现这一点,目的是使用为scipy CSR矩阵实现的快速向量操作。 A和B的行是标准化的,所以为了计算余弦相似度,我只需要找到A中每一行与B中每一行的点积。A的

    • C++:15秒(源) Python:6分13秒(来源) C++:45分钟(源) 蟒蛇:10小时后被杀死(来源) 为什么Strassen矩阵乘法比标准矩阵乘法慢得多? null null null

    • 我正在尝试使用OMP运行矩阵乘法程序。我在串行和并行版本中得到了不同的输出。我正在尝试使用一个3*3矩阵进行测试。 我的并行代码是: 对于串行版本,我刚刚注释了该行: 我的并行版本的输出是: 起始矩阵 具有 12 个线程的多个示例初始化矩阵...线程 0 起始矩阵乘...线程 8 起始矩阵乘法...线程 6 起始矩阵乘...线程 9 起始矩阵乘法...线程 5 起始矩阵乘法...线程 1 起始矩阵

    • 本文向大家介绍C++稀疏矩阵的各种基本运算并实现加法乘法,包括了C++稀疏矩阵的各种基本运算并实现加法乘法的使用技巧和注意事项,需要的朋友参考一下 代码: 总结 以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,谢谢大家对呐喊教程的支持。如果你想了解更多相关内容请查看下面相关链接

    • 主要内容:逐元素矩阵乘法,矩阵乘积运算,矩阵点积矩阵乘法是将两个矩阵作为输入值,并将 A 矩阵的行与 B 矩阵的列对应位置相乘再相加,从而生成一个新矩阵,如下图所示: 注意:必须确保第一个矩阵中的行数等于第二个矩阵中的列数,否则不能进行矩阵乘法运算。 图1:矩阵乘法 矩阵乘法运算被称为向量化操作,向量化的主要目的是减少使用的 for 循环次数或者根本不使用。这样做的目的是为了加速程序的计算。 下面介绍 NumPy 提供的三种矩阵乘法,从而进一步