32x32乘以并添加优化

时间:2010-07-27 07:29:03

标签: c optimization loops

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

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;



}

ode here

9 个答案:

答案 0 :(得分:5)

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

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

答案 1 :(得分:3)

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

正如其他人评论的那样,你的内循环看起来很适合矢量处理指令(如SSE,如果你在x86上)。检查编译器的内在函数。

答案 2 :(得分:2)

我不认为你可以用C来优化它。你的编译器可能有生成SIMD代码的选项,但如果性能很关键,你可能只需要编写自己的SIMD汇编代码...... < / p>

答案 3 :(得分:1)

您可以使用极少数SSE2内在函数替换内循环

见[_mm_madd_epi16] [1]替换八个

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

和[_mm_add_epi16] [2]或_ [mm_add_epi32] [3]取代八个

rgiFilter[] += rgiUpdate[];

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

这些内在函数特定于Microsoft和英特尔编译器。 我相信GCC的等价物我只是没有使用它们。

编辑:根据以下评论,我会更改以下内容......

如果你有混合类型,编译器并不总是很聪明,无法弄明白。 我建议以下内容使其更明显,并给它一个更好的机会 在autovectorizing。

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

  5. 这应该足以让英特尔编译器弄明白

答案 4 :(得分:0)

  1. 确保将iPred保存在寄存器中(之前不从内存中读取,并且在每次+ =操作后不将其写回内存)。
  2. 优化第一级缓存的内存布局。确保3个阵列不争用相同的缓存条目。这取决于CPU架构,并不简单。

答案 5 :(得分:0)

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

请参阅Gcc Auto-vectorization

答案 6 :(得分:0)

首先确保数据在内存中线性布局,这样就不会出现缓存未命中。这似乎不是一个问题。

如果你不能SSE操作(如果编译器失败了 - 看看程序集),尝试分成几个不同的小循环(每个0 ... 8一个)。编译器倾向于能够对执行较少操作的循环进行更好的优化(除非在这样的情况下,它可以进行矢量化/ SSE)。

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

答案 7 :(得分:0)

相当不错的代码。

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

其他建议很好。另外,我有时会发现如果将这些活动分成不同的循环,我会得到更快的代码,比如

  • 进行乘法的一个循环并保存到临时数组。

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

  • rgiUpdate添加到rgiFilter的一个循环。

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

答案 8 :(得分:0)

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

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

变化:

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参数是告诉编译器您是否要读取或写入地址。如果数据在您访问后需要保留在缓存中,则与地方有关。编译器只是试图做到最好,它可以弄清楚如何为此生成正确的指令,但如果它不能做你在某个目标上所要求的,它就什么都不做,它不会伤害任何东西。< / p>

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

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