不确定如何解释我的并行化矩阵乘法代码

时间:2016-01-19 06:13:33

标签: c++ performance parallel-processing openmp matrix-multiplication

我在OpenMP中运行此代码进行矩阵乘法,我测量了它的结果:

#pragma omp for schedule(static)
for (int j = 0; j < COLUMNS; j++)
    for (int k = 0; k < COLUMNS; k++)
        for (int i = 0; i < ROWS; i++)
            matrix_r[i][j] += matrix_a[i][k] * matrix_b[k][j];

根据我放置#pragma omp指令的位置 - 在j循环,k循环或i循环之前,有不同版本的代码。此外,对于每个变体,我为默认静态调度运行了不同的版本,使用块1和10运行静态调度,使用相同的块运行动态调度。我还测量了CodeXL中的DC访问,DC未命中,CPU时钟,退役指令和其他性能指标的数量。以下是AMD Phenom I X4 945上尺寸为1000x1000的矩阵的结果:

Results of performance measurements

multiply_matrices_1_dynamic_1在第一个循环之前是#pragma omp的函数,在第一个循环之前是动态调度,等等。以下是我对结果不太了解的一些事情,并希望得到帮助:< / p>

  • 内循环运行的默认静态版本在2,512s之前运行,而顺序版本在31,683s运行 - 尽管它运行在4核机器上,所以我想到最大可能的加速会是4倍。这在逻辑上是否可能发生,或者这是一些错误?我怎么解释呢?
    • CodeXL表示,静态调度的第3版本的DC访问(和未命中)数量远低于其他版本。这是为什么?主要不是因为所有并行线程都在b矩阵的同一个单元上运行。是吗?
    • 我知道第二个版本是不正确的,因为线程可能在R矩阵的同一个单元上运行,这可能会导致竞争条件。但是为什么它的性能会降低呢?不正确会以某种方式导致它吗?
    • 我意识到动态调度会导致开销,这就是使用它时代码速度变慢的原因。此外,对于第3个版本(在i循环之前使用pragma),调度执行的次数要多很多次(使用1000x1000矩阵进行十亿次),因此算法的性能要低得多。但是,这个调度似乎不会导致第二个版本减速(在第二个循环之前使用pragma)。那是为什么?

另外,我对TLB未命中缓存未命中的关系感到困惑。什么时候使用DTLB?我教授的文档说每次DC访问都是DTLB请求,但我不明白它是如何工作的 - TLB未命中数通常大于DC访问次数。如何计算TLB未命中率?我的教授说这是TBL未命中/ DC访问。他还说我可以通过缓存命中率和空间位置按TLB命中率来计算时间局部性。这是如何工作的?

2 个答案:

答案 0 :(得分:2)

Gilles有正确的想法,你的代码缓存不友好,但他的解决方案仍然has a similar problem because it does the reduction over k on matrix_b[k][j]

一种解决方案是计算matrix_b的转置,然后您可以matrix_bT[j][k]超过k,这是缓存友好的。转置为O(n^2)),矩阵乘法为O(n^3),因此转置的成本为1/n。即对于较大的n,它变得可以忽略不计。

但是使用转置比使用转置更容易。像这样减少j

#pragma omp for schedule(static)
for (int i = 0; i < ROWS; i++ ) {
    for (int k = 0; k < COLUMNS; k++ ) {
        for ( int j = 0; j < COLUMNS; j++ ) {
           matrix_r[i][j] += matrix_a[i][k]*matrix_b[k][j];
        }
    }
}

吉勒&#39;每次迭代时,方法需要从内存中读取两次,而这种解决方案每次迭代需要两次读取和一次内存写入,但它具有更多的缓存友好性,这足以弥补对内存的写入。

答案 1 :(得分:1)

我不确定你的数字显示的是什么,但我确信你的代码,就像现在写的那样,几乎一样无效。因此,在你使代码合理有效之前,讨论关于这个或那个计数器数字的细节将毫无意义。

我声称您的代码无效的原因是因为您组织循环的顺序可能是最糟糕的:对数据的访问都不是线性的,导致缓存的使用效率极低。通过简单地围绕循环进行交换,您应该显着提高性能,并开始考虑可以做些什么来进一步改进它。

例如,这个版本应该已经好多了(未经测试):

#pragma omp for schedule( static )
for ( int i = 0; i < ROWS; i++ ) {
    for ( int j = 0; j < COLUMNS; j++ ) {
        auto res = matrix_r[i][j]; // IDK the type here
        #pragma omp simd reduction( + : res )
        for ( int k = 0; k < COLUMNS; k++ ) {
           res += matrix_a[i][k] * matrix_b[k][j];
        }
        matrix_r[i][j] = res;
    }
}

(注意:我添加simd指令只是因为它看起来合适,但这不是重点:

从那里开始,尝试循环折叠,线程调度和/或循环平铺将开始有意义。