【问题标题】:Efficient by-hand loop unrolling高效的手动循环展开
【发布时间】:2011-05-16 22:33:17
【问题描述】:

我有这个 C 代码:

for (k = 0; k < n_n; k++) {
    if (k == i || k == j) continue;
    dd=q2_vect[k]-q1_vect;
    d2=dd*dd;
    if (d2<0) {
        a=1;
        break;
    }       
}  

出于编译器优化的原因(在 Cell 处理器的 SPE 上),我需要手动解循环,所以我尝试了:

dd=q2_vect[0]-q1_vect;
d2=dd*dd;
if (d2<0)goto done;

dd=q2_vect[1]-q1_vect;
d2=dd*dd;
if (d2<0)goto done;

dd=q2_vect[2]-q1_vect;
d2=dd*dd;
if (d2<0)goto done;

.....
.....

// end
goto notdone;

done: 
ok=0;

notdone:
.....

但是我不知道怎么处理

if (k == i || k == j) continue;

事实上,lopp 取决于“n_n”上的每次运行,并且我应该手动编写代码多次,以达到最大值“n_n”的值。

您认为如何解决?

【问题讨论】:

  • 序列if d2&lt;0是无效的C.
  • 即使你添加了必要的括号,一个好的编译器也会优化整个 if 语句,因为 C 语言没有定义它可能为真的条件。 (代码似乎试图依赖未定义的行为。)
  • 不正确:dd == _Imaginary_I -> dd*dd == -1.0 这是有效的 C.
  • 对不起,我打错了,是 d2>0
  • 所以您正在寻找除ij 之外的第一个索引,其中d2 不为零?你为什么要乘法得到d2?然后条件等价于q2_vect[k]!=q1_vect...

标签: c optimization loops unroll


【解决方案1】:

您确定编写的代码是正确的吗?如果dd 是有符号整数类型,则当前代码具有未定义的行为,并且如果d2 是无符号的或者ddd2 是浮点类型,则永远不会满足if 中的条件。看起来您正在对除 ij 之外的第一个索引 k 进行错误搜索,其中表达式 q2_vect[ k]-q1_vect 的平方溢出。

至于有效地跳过ij 迭代,我将只查看展开的“循环”停止的位置,如果k 等于i 或@,则在k+1 重新启动它987654334@。这是假设您的循环中的代码没有副作用/运行总和,这是真实的,但我希望您可能打算让代码做其他事情(比如对平方求和)。

最后,我非常怀疑你是否希望手动展开循环,因为你甚至似乎没有工作代码开始。任何优秀的编译器都可以为您展开循环,但您想要执行的循环展开类型通常会使性能更差而不是更好。我认为你最好先让你的代码正常工作,然后测量(并查看编译器生成的 asm),然后只在确定存在问题之后尝试改进那个 .

【讨论】:

  • 你确定编译器会自动展开这个循环吗?除非 n_n 是一个常数,否则我认为编译器将很难确定如何展开它。
  • 如果 dd 是 _Imaginary 并且 d2 是真实类型,则可能满足条件(没有未定义的行为)。 :)
  • 完全展开循环几乎没有用处。相反,编译器可能会展开它以在每次运行时执行 8 或 16 次迭代,并使用类似于 duff 设备的构造来处理尾部。
  • @Vovanium:哈哈,很公平。希望我能给你超过+1的注意。 OP 真的使用复杂类型吗??
  • @R..:作者当然不太可能依赖虚数,但许多人认为 x*x>=0 是不正确的。我很生气。 :)
【解决方案2】:

编写的这段代码非常不适合 SPE,因为它的分支非常多。此外,有关所涉及变量类型的信息也会有所帮助;编写的测试似乎相当模糊(即使使用&gt;0 修复),但代码看起来可能是C++,使用某种向量类重载operator - 表示向量减法和operator * 两个向量的计算一个点积。

在 SPE 上处理这种简单循环的第一件事是让它们无分支(至少是内部循环;即展开几次,每 N 次迭代只检查是否提前退出)并使用 SIMD 指令:SPE 只有 SIMD 指令,因此不在循环中使用 SIMD 处理会立即浪费 75% 的可用寄存器空间和计算能力。同样,SPE 一次只能加载对齐的 qwords(16 字节),使用较小的数据类型需要额外的工作来打乱寄存器的内容,以便您尝试加载的值最终位于“首选插槽”中。

您可以通过使用以下无分支形式重写循环的第一部分来摆脱if (k == i || k == j)(这是伪代码。它立即适用于整数,但您需要使用内在函数来获得按位操作浮动):

dd = q2_vect[k] - q1_vect;
d2 = dd * dd;
d2 &= ~(cmp_equal(k, i) | cmp_equal(k, j));

这里,cmp_equal 对应于各自的 SPE 内在函数(语义:cmp_equal(a,b) == (a == b) ? ~0u : 0)。当k == ik == j 时,这会强制d2 为零。

要避免内部循环中的if (d2 &gt; 0) 分支,请执行以下操作:

a |= cmp_greater(d2, 0);

并且仅检查a 是否为非零(提前退出)每几次循环迭代。如果为d2 计算的所有值都是非负的(如果您的类型是整数、浮点数或实值向量类,则会出现这种情况),您可以进一步简化此操作。做吧:

a |= d2;

最后,a 只有在所有单个项都非零时才会非零。但要小心整数溢出(如果您使用整数)和 NaN(如果您使用浮点数)。如果你必须处理这些情况,上面的简化会破坏代码。

【讨论】:

  • +1 用于 SIMD。值得注意的是,如果对循环进行矢量化处理,当您将 kij 进行比较时,k 将在每个元素中具有不同的值,例如它将从 {0,1,2,3} 开始,每次递增 {4,4,4,4}。此外,spu_orx(“OR 跨越”)内在函数在此处可能用于检查是否设置了任何比较结果。
【解决方案3】:

通常循环展开意味着使循环包含一些迭代,从而减少运行次数。例如,

for(i=0;i<count;i++) {
    printf("%d", i);
}

可以展开到

i=0;
if(count%2==1) {
    printf("%d", i);
    i=1;
}
while(i<count) {
    printf("%d", i);
    printf("%d", i+1);
    i+=2;
}

【讨论】:

  • 这里我不明白或看到的,是编译器如何优化它
【解决方案4】:

对于第一个问题,你不需要在满足条件时“执行”循环体。对于这个特殊问题,您可以将该条件的逻辑否定放在if 语句的条件中。

通常展开是一个因素;展开的代码仍然存在于循环中(除非已知循环边界非常小)。此外,您需要在循环之外完成工作的“剩余部分”(对应于问题大小除以展开因子的剩余部分)。

所以,一个循环展开的例子:

for (i = 0; i < n; ++i) do_something(i);

可以展开 2 倍至:

for (i = 0; i < n-1; i += 2) { do_something(i); do_something(i+1); }
for (; i < n; ++i) do_something(i);

第二个循环执行“剩余”的位置(它还将i 设置为与展开循环相同的内容,但如果在此之后不需要i,则整行可以是@987654326 @对于这种情况)。

【讨论】:

    【解决方案5】:

    假设 n_n 是一个编译时常量,循环可能会像这样简单展开:

    do
    { 
      k=0
      if (k == i || k == j) 
        ;
      else
      {
        dd=q2_vect[ k]-q1_vect;
        d2=dd*dd;
        if (d2<0)
        {
          a=1;
          break;
        }
      }
    
      k=1
      if (k == i || k == j) 
        ;
      else
      {
        dd=q2_vect[ k]-q1_vect;
        d2=dd*dd;
        if (d2<0)
        {
          a=1;
          break;
        }
      }
    
      /* and so on, n_n times */
    
      k= n_n-1
      if (k == i || k == j) 
        ;
      else
      {
        dd=q2_vect[ k]-q1_vect;
        d2=dd*dd;
        if (d2<0)
        {
          a=1;
          break;
        }
      }
    
    } while (0);
    

    基本上, continue 之后的所有内容都进入 if 语句的 else 部分

    编辑:由于n_n 不是编译时间常数,您仍然可以通过在循环中执行多次循环来展开循环,然后以 switch-case 语句结束。其实你可以这样组合,这叫Duff's device.

    #define LOOP_BODY              \
    do{                            \  
      if (k == i || k == j)        \
        ;                          \
      else                         \
      {                            \
        dd=q2_vect[ k]-q1_vect;    \
        d2=dd*dd;                  \
        if (d2<0)                  \
        {                          \
          a=1;                     \
          break;                   \
        }                          \
      } while (0)          
    
    
    k = 0;
    switch(n_n % 8)
    {
      case 0: for (; k < n_n; k++) { LOOP_BODY; k++; 
      case 7:                        LOOP_BODY; k++;
      case 6:                        LOOP_BODY; k++;
      case 5:                        LOOP_BODY; k++;
      case 4:                        LOOP_BODY; k++;
      case 3:                        LOOP_BODY; k++;
      case 2:                        LOOP_BODY; k++;    
      case 1:                        LOOP_BODY; k++;}
    }
    

    【讨论】:

    • 问题是:n_n 是一个运行时值,由用户输入。如果我不知道值,如何同时手动展开循环 n_n 次?
    【解决方案6】:

    展开这个循环在这里没有多大帮助。内循环软件展开有助于指令的软件流水线化,以在运行时实现更高的 IPC。在这里,它可能会通过展开来破坏逻辑。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2010-09-16
      • 2020-03-18
      • 1970-01-01
      • 2023-03-09
      • 1970-01-01
      • 2016-01-03
      相关资源
      最近更新 更多