竞争条件会降低代码的性能吗?

Posted

技术标签:

【中文标题】竞争条件会降低代码的性能吗?【英文标题】:Can race conditions lower the code's performance? 【发布时间】:2016-01-19 22:02:19 【问题描述】:

我正在为矩阵乘法运行以下代码,我应该测量其性能:

for (int j = 0; j < COLUMNS; j++)
#pragma omp for schedule(dynamic, 10)
    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];

是的,我知道它真的很慢,但这不是重点——它纯粹是为了性能测量目的。我正在运行 3 个版本的代码,具体取决于我放置 #pragma omp 指令的位置,因此取决于并行化发生的位置。代码在 Microsoft Visual Studio 2012 中以发布模式运行,并在 CodeXL 中进行分析。

我从测量中注意到的一件事是,代码 sn-p 中的选项(在 k 循环之前进行并行化)是最慢的,然后是在 j 循环之前带有指令的版本,然后是带有它的版本在 i 循环之前。所呈现的版本也是由于竞争条件而计算错误结果的版本 - 多个线程同时访问结果矩阵的同一单元格。我理解为什么 i 循环版本是最快的 - 所有特定线程只处理 i 变量范围的一部分,从而增加了时间局部性。但是,我不明白是什么导致 k 循环版本最慢 - 它是否与它产生错误结果的事实有关?

【问题讨论】:

你为什么不像I explained yesterday那样交换内循环和外循环。实现起来很简单。 【参考方案1】:

当然,竞争条件会减慢代码速度。当两个或多个线程访问内存的同一部分(相同的缓存行)时,该部分必须一遍又一遍地加载到给定内核的缓存中,因为另一个线程通过写入缓存的内容使其无效。他们争夺共享资源。

当两个在内存中距离太近的变量被更多线程读写时,也会导致速度变慢。这被称为false sharing。在您的情况下,情况更糟,它们不仅靠得太近,甚至还重合。

【讨论】:

这当然取决于平台。 x86 将倾向于在写入后使其他内核的缓存无效。 ARM 不会没有程序员的明确要求。 我没有真正使用过 x86 以外的 RISC HPC 平台。过去有一些 SPARC,但当时我对这些问题一无所知。现在 x86 甚至在 IBM 和 Cray 超级计算机中统治着世界,过去人们可以在其中找到 Power 或其他 CPU。 SGI MIPS 也死了。 不过还没见过 ARM 并行计算机。【参考方案2】:

你的假设是正确的。但是,如果我们谈论的是性能,而不仅仅是验证您的假设,那么还有更多的故事。

索引的顺序是个大问题,不管是否多线程。假设mat[x][y]mat[x][y+1] 之间的距离为1,而mat[x][y]mat[x+1][y] 之间的距离为dim(mat[x]) 您希望x 成为外部索引,y 内部具有最小迭代之间的距离。给定__[i][j] += __[i][k] * __[k][j];,您会看到空间位置的正确顺序是i -&gt; k -&gt; j

无论顺序如何,都有一个值可以保存以备后用。鉴于你的 sn-p

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

matrix_b[k][j] 值将从内存中提取i 次。你可以从

开始
    for (int j = 0; j < COLUMNS; j++)
        for (int k = 0; k < COLUMNS; k++)
            int temp = matrix_b[k][j];
            for (int i = 0; i < ROWS; i++)
                matrix_r[i][j] += matrix_a[i][k] * temp;

但鉴于您正在写入matrix_r[i][j],优化的最佳访问是matrix_r[i][j],因为写入比读取慢

对内存的不必要写访问

for (int i = 0; i < ROWS; i++)
    matrix_r[i][j] += matrix_a[i][k] * matrix_b[k][j];

将写入内存matrix_r[i][j]ROWS 次。使用临时变量会减少对一个变量的访问。

    for (int i = 0; i < ...; j++)
        for (int j = 0; j < ...; k++)
            int temp = 0;
            for (int k = 0; k < ...; i++)
                temp += matrix_a[i][k] * matrix_b[k][j];
            matrix_r[i][j] = temp;

这将写访问从 n^3 减少到 n^2。

现在您正在使用线程。为了最大限度地提高多线程的效率,您应该尽可能多地将线程内存访问与其他线程隔离。一种方法是给每个线程一个列,并完善该列一次。一种简单的方法是对matrix_b 进行转置,这样

matrix_r[i][j] += matrix_a[i][k] * matrix_b[k][j]; becomes 
matrix_r[i][j] += matrix_a[i][k] * matrix_b_trans[j][k];

这样 k 上的最内层循环总是处理与 matrix_amatrix_b_trans 相关的连续内存

    for (int i = 0; i < ROWS; j++)
        for (int j = 0; j < COLS; k++)
            int temp = 0;
            for (int k = 0; k < SAMEDIM; i++)
                temp += matrix_a[i][k] * matrix_b_trans[j][k];
            matrix_r[i][j] = temp;

【讨论】:

@JeremyFriesner,不,编译器不会重新排列循环并创建临时变量。这里正确的解决方案是交换最内层和最外层循环。 当然,我无法与您的特定编译器交谈,但总的来说,C++ 优化器允许进行这些优化(只要优化不改变任何程序的用户可见行为),而现代 C++ 编译器(g++、MSVC、clang 等)通常会实现它们。但不要相信我的话,这里有一些描述它的链接:en.wikipedia.org/wiki/Loop_optimizationcompileroptimizations.com/category/hoisting.htm @JeremyFriesner 虽然编译器有时可以,但通常不可能,因为编译器认为对循环重新排序会产生不同的结果(由于浮点运算不是关联的)。尽可能自己重新排序循环绝对是个好主意。 @Zboson:我读到其中一个 SPEC 基准测试被编译器进行循环交换优化“破坏”。我忘记了细节,但 IIRC Sun 是第一个管理它的供应商。他们的compile options for SPECcpu2000 记录了一些关于循环交换的内容。我确实记得读过这种优化非常脆弱,并且主要只适用于与那个 SPEC 组件中的那个循环的源非常相似的东西。我认为这与 462.libquantum 并行化的事情不同。 @PeterCordes,我开始想知道编译器何时会开始使用机器学习进行优化(我们可以称之为深度优化)。我猜 GCC 已经可以在某种程度上通过分析实现这一点,但我从未尝试过。也许 JIT 编译器(我的意思是例如 C#/Java)已经这样做了。

以上是关于竞争条件会降低代码的性能吗?的主要内容,如果未能解决你的问题,请参考以下文章

这段代码中有竞争条件吗?

过度使用硬盘会降低硬盘驱动器的性能吗?

JAVA反射会降低你的程序性能吗?

渲染更多部分会降低 Rails Web 应用程序的性能吗?

为啥条件断点会在调试时降低应用程序的执行速度?

低代码开发,这是企业数字化的未来吗?