【问题标题】:32x32 Multiply and add optimization32x32 乘法和加法优化
【发布时间】:2011-03-21 11:14:08
【问题描述】:

我正在优化应用程序。我发现我需要优化内部循环以提高性能。 rgiFilter 是一个 16 位的数组。

for (i = 0; i < iLen; i++) {
    iPredErr = (I32)*rgiResidue;
    rgiFilter = rgiFilterBuf;
    rgiPrevVal = rgiPrevValRdBuf + iRecent;
    rgiUpdate = rgiUpdateRdBuf + iRecent;

    iPred = iScalingOffset;

    for (j = 0; j < iOrder_Div_8; j++) {


                 iPred += (I32) rgiFilter[0] * rgiPrevVal[0]; 
                 rgiFilter[0] += rgiUpdate[0];

                 iPred += (I32) rgiFilter[1] * rgiPrevVal[1]; 
                 rgiFilter[1] += rgiUpdate[1];

                 iPred += (I32) rgiFilter[2] * rgiPrevVal[2]; 
                 rgiFilter[2] += rgiUpdate[2];

                 iPred += (I32) rgiFilter[3] * rgiPrevVal[3]; 
                 rgiFilter[3] += rgiUpdate[3];

                 iPred += (I32) rgiFilter[4] * rgiPrevVal[4]; 
                 rgiFilter[4] += rgiUpdate[4];

                 iPred += (I32) rgiFilter[5] * rgiPrevVal[5]; 
                 rgiFilter[5] += rgiUpdate[5];

                 iPred += (I32) rgiFilter[6] * rgiPrevVal[6]; 
                 rgiFilter[6] += rgiUpdate[6];

                 iPred += (I32) rgiFilter[7] * rgiPrevVal[7]; 
                 rgiFilter[7] += rgiUpdate[7];

                    rgiFilter += 8;
        rgiPrevVal += 8;
                    rgiUpdate += 8;



}

在这里颂

【问题讨论】:

  • 能否提供为此特定循环生成的程序集?
  • 任何分析器输出也会很有趣(l1/l2 缓存未命中、指令未命中、LHS 等)。

标签: c optimization loops


【解决方案1】:

您可以用很少的 SSE2 内在函数替换内部循环

参见 [_mm_madd_epi16][1] 替换八个

iPred += (I32) rgiFilter[] * rgiPrevVal[];

和 [_mm_add_epi16][2] 或 _[mm_add_epi32][3] 替换八个

rgiFilter[] += rgiUpdate[];

你应该会看到一个很好的加速。

这些内在函数特定于 Microsoft 和 Intel 编译器。 我确信 GCC 存在等价物,我只是没有使用它们。

编辑:根据下面的 cmets,我将更改以下内容...

如果你有混合类型,编译器并不总是足够聪明来解决它。 我建议以下内容使其更加明显并给它一个更好的机会 在自动矢量化。

  1. 将 rgiFilter[] 声明为 I32 位 此功能的目的。你 将支付一份。
  2. 将 iPred 更改为 iPred[] 作为 I32 也
  3. 在内部(甚至外部)循环之外执行 iPred[] 求和

  4. 将类似的指令打包成四个一组

    iPred[0] += rgiFilter[0] * rgiPrevVal[0];

    iPred[1] += rgiFilter[1] * rgiPrevVal[1];

    iPred[2] += rgiFilter[2] * rgiPrevVal[2];

    iPred[3] += rgiFilter[3] * rgiPrevVal[3];

    rgiFilter[0] += rgiUpdate[0];

    rgiFilter[1] += rgiUpdate[1];

    rgiFilter[2] += rgiUpdate[2];

    rgiFilter[3] += rgiUpdate[3];

这应该足以让英特尔编译器弄清楚

【讨论】:

  • 但是你看,_mm_madd_epi16 做了 16x16 乘法。我需要 32x32 .. 我尝试将 32x32 乘法分解为一系列 16x16 乘法。但是涉及到太多的数据处理,我怀疑它会提高我的表现。
  • P.s 目标是带有 SSE3 指令的 Intel Atom。 SSE4 有一个完美的 32x32 乘法内在函数。但我没有那个选项。 :(
  • P ps:我也在使用英特尔的 icc... 11.0 版。这是我第一次使用它..
【解决方案2】:

您可以进行很多优化,包括引入特定于目标的代码。不过,我将主要使用通用的东西。

首先,如果您要使用索引限制循环,那么您通常应该尝试向下循环。

变化:

for (i = 0; i < iLen; i++) {

for (i = iLen-1; i <= 0; i--) {

这可以利用许多常见处理器本质上对任何数学运算的结果与 0 进行比较的事实,因此您不必进行显式比较。

不过,这仅适用于向后遍历循环具有相同结果并且索引已签名的情况(尽管您可以偷偷摸摸)。

或者,您可以尝试通过指针数学进行限制。这可能会消除对显式索引(计数器)变量的需求,这可以加快处理速度,尤其是在寄存器供不应求的情况下。

for (p = rgiFilter; p <= rgiFilter+8; ) {
     iPred += (I32) (*p) + *rgiPreval++;
     *p++ += *rgiUpdate++;

     ....

}

这也消除了内部循环结束时的奇怪更新。循环结束时的更新可能会混淆编译器并使其产生更糟糕的代码。您可能还会发现,您所做的循环展开可能会产生更差或同样好的结果,就好像您在内部循环的主体中只有两个语句一样。编译器可能能够就如何滚动/展开此循环做出正确的决定。或者您可能只想确保循环展开两次,因为 rgiFilter 是一个 16 位值的数组,并查看编译器是否可以利用访问它两次来完成两次读取和两次写入 - 执行一次 32 位加载和一个 32 位存储。

for (p = rgiFilter; p <= rgiFilter+8; ) {
     I16 x = *p;
     I16 y = *(p+1); // Hope that the compiler can combine these loads
     iPred += (I32) x + *rgiPreval++;
     iPred += (I32) y + *rgiPreval++;

     *p++ += *rgiUpdate++;
     *p++ += *rgiUpdate++; // Hope that the complier can combine these stores

     ....

}

如果您的编译器和/或目标处理器支持它,您也可以尝试发出预取指令。例如 gcc 有:

__builtin_prefetch (const void * addr)
__builtin_prefetch (const void * addr, int rw)
__builtin_prefetch (const void * addr, int rw, int locality)

这些可以用来告诉编译器如果目标有预取指令,它应该使用它们来尝试继续并将addr 放入缓存中。最佳情况下,这些应该在您正在处理的每个阵列的每个缓存行步骤中发出一次。 rw 参数是告诉编译器你是想读还是写地址。本地性与您访问数据后是否需要保留在缓存中有关。编译器只是尽力做到最好,它可以弄清楚如何为此生成正确的指令,但如果它不能在某个目标上执行您要求的操作,它就什么也不做,也不会伤害任何东西。

此外,由于 __builtin_ 函数是特殊的,所以关于可变参数数量的常规规则并不真正适用——这是对编译器的提示,而不是对函数的调用。

您还应该查看您的目标支持的任何向量操作,以及您的编译器支持执行向量操作的任何通用或平台特定函数、内置函数或编译指示。

【讨论】:

    【解决方案3】:

    相当不错的代码。

    在每一步,你基本上都在做三件事,一个乘法和两个加法。

    其他建议都不错。此外,我有时会发现,如果我将这些活动分成不同的循环,我会获得更快的代码,例如

    • 一个循环来做乘法并保存到一个临时数组中。

    • 一个循环来对iPred中的数组求和。

    • 一个循环将rgiUpdate 添加到rgiFilter

    通过展开,您的循环开销可以忽略不计,但如果每个循环内完成的不同事情的数量最小化,编译器有时可以更好地利用其寄存器。

    【讨论】:

      【解决方案4】:

      首先要确保数据在内存中呈线性布局,以免缓存未命中。不过,这似乎不是问题。

      如果您无法对操作进行 SSE(并且如果编译器失败 - 查看程序集),请尝试将其分成几个较小的不同 for 循环(每个 0 .. 8 一个)。编译器往往能够对执行较少操作的循环进行更好的优化(除非在这种情况下它可能能够进行矢量化/SSE)。

      16 位整数对于 32/64 位架构来说更昂贵(除非它们具有特定的 16 位寄存器)。在执行循环之前尝试将其转换为 32 位(大多数 64 位架构也有 32 位寄存器)。

      【讨论】:

        【解决方案5】:

        循环展开和向量化应该留给编译器。

        Gcc Auto-vectorization

        【讨论】:

          【解决方案6】:

          如果rgiFilterBufrgiPrevValRdBufrgiUpdateRdBuf 是没有别名的函数参数,请使用restrict 限定符声明它们。这将允许编译器进行更积极的优化。

          正如其他人评论的那样,您的内部循环看起来可能非常适合矢量处理指令(如 SSE,如果您使用的是 x86)。检查编译器的内在函数。

          【讨论】:

            【解决方案7】:
            1. 确保 iPred 保存在寄存器中(之前没有从内存中读取,也没有在每次 += 操作后写回内存中)。
            2. 优化一级缓存的内存布局。确保 3 个数组不会争夺相同的缓存条目。这取决于 CPU 架构,一点也不简单。

            【讨论】:

              【解决方案8】:

              您唯一的选择是一次执行多个操作,这意味着以下 3 个选项之一:

              1. SSE 指令 (SIMD)。您可以使用一条指令处理多个内存位置
              2. 多线程 (MIMD)。如果您有 1 个以上的 cpu 内核,这种方法效果最好。将您的数组拆分为多个彼此独立的大小相似的条带(依赖性将大大增加此选项的复杂性,如果您需要大量锁,则比按顺序计算所有内容要慢)。请注意,数组必须足够大以抵消额外的上下文切换和同步开销(它非常小,但不可忽略)。最适合 4 核或更多核。
              3. 两者兼而有之。如果你的数组真的很大,你可以通过结合两者来获得很多。

              【讨论】:

                【解决方案9】:

                我认为您无法在 C 中优化它。您的编译器可能具有生成 SIMD 代码的选项,但如果性能至关重要,您可能需要编写自己的 SIMD 汇编代码...

                【讨论】:

                  猜你喜欢
                  • 1970-01-01
                  • 1970-01-01
                  • 2015-08-14
                  • 1970-01-01
                  • 2010-09-14
                  • 1970-01-01
                  • 2012-04-11
                  • 2013-03-10
                  相关资源
                  最近更新 更多