循环展开什么时候有效?

时间:2008-10-10 09:59:56

标签: c++ performance algorithm optimization

Loop unwinding是帮助编译器优化性能的常用方法。我想知道是否以及在多大程度上性能增益受到循环体内的影响:

  1. 陈述数
  2. 函数调用次数
  3. 使用复杂数据类型,虚拟方法等
  4. 动态(de)内存分配
  5. 您使用什么规则(拇指?)来决定是否要解除性能关键循环?在这些情况下,您还考虑了哪些其他优化?

10 个答案:

答案 0 :(得分:32)

一般来说,手动展开循环是不值得的。编译器更好地了解目标体系结构的工作原理,并在有益的情况下展开循环。

有些代码路径在Pentium-M型CPU的展开时会受益,但是例如对Core2没有好处。如果我手动展开,编译器就不能再做出决定了,我最终可能会得到不太理想的代码。例如。正好与我试图实现的相反。

在某些情况下,我会手动展开性能关键循环,但如果我知道编译器在手动展开后能够使用架构特定功能(如SSE或MMX指令),我就会这样做。然后,我才这样做。

顺便说一句 - 现代CPU在执行可预测的分支方面非常有效。这正是一个循环。这些循环开销如此之小,以至于很少有所作为。然而,由于代码大小的增加而可能发生的内存延迟效应会产生影响。

答案 1 :(得分:14)

这是一个优化问题,因此只有一条经验法则:测试性能,如果您的测试证明您需要,请尝试循环展开优化 。首先考虑破坏性较小的优化。

答案 2 :(得分:7)

根据我的经验,循环展开,并且在以下情况下所需的工作是有效的:

  • 循环中只有几个语句。
  • 语句只涉及少量不同的变量而没有函数调用
  • 您的操作适用于已分配的内存(例如,就地图像转换)

对于80%的收益,部分放松通常较少。因此,不是循环N×M图像的所有像素(N M次迭代),其中N总是可被8整除,在8个像素的每个块上循环(N M / 8)次。如果您正在执行一些使用某些相邻像素的操作,这将特别有效。

我已经获得了非常好的结果,手工优化像素操作到MMX或SSE指令(一次8或16像素)但我也花了几天时间优化一些东西只是为了找出优化的版本编译器的运行速度提高了十倍。

顺便说一句,对于循环展开的最(美丽)的例子,请查看Duffs device

答案 3 :(得分:4)

需要考虑的重要事项:在工作场所的生产代码中,代码的未来可读性远远超过循环展开的好处。硬件很便宜,程序员的时间不是。我只担心循环展开,如果这是解决已证实的性能问题的唯一方法(比如在低功耗设备中)。

其他想法:编译器的特性差异很大,在某些情况下,像Java一样,HotspotJVM会立即做出决定,因此我无论如何都会反对循环展开。

答案 4 :(得分:2)

这些优化高度依赖于代码执行的cpu,应由编译器完成,但如果您正在编写这样的编译器,您可能需要查看英特尔文档Intel(R) 64 and IA-32 Architectures Optimization Reference Manual部分3.4.1.7:

  • 展开小循环,直到分支和归纳变量的开销(通常)占用循环执行时间的不到10%。

  • 避免过度展开循环;这可能会破坏跟踪缓存或指令缓存。

  • 展开经常执行且具有可预测迭代次数的循环,以将交互次数减少到16或更少。这样做除非它增加代码大小,以使工作集不再适合跟踪或指令缓存。如果循环体包含多个条件分支,则展开以使迭代次数为16 /(#condtional branches)。

您还可以免费订购一份硬拷贝here

答案 5 :(得分:1)

在较新的处理器上手动展开循环可能效率不高但它们仍然可用于GPU和轻型架构(如ARM),因为它们在预测时不如当前一代CPU处理器好,而且测试和跳转实际上会浪费在这些处理器上

也就是说,它应该只在非常紧凑的循环和块中完成,因为通过展开你的代码大小非常膨胀,这将在小型设备上烧掉缓存,你最终会遇到一个更糟糕的问题

但请注意,在优化时,展开循环应该是最后的手段。它会使您的代码在一定程度上变得无法维护,并且有人阅读它可能会破坏并威胁您和您的家人。知道这一点,让它值得:)

宏的使用可以极大地帮助使代码更具可读性,并且会使注销成为故意。

示例:

for(int i=0; i<256; i++)
{
    a+=(ptr + i) << 8;
    a-=(ptr + i - k) << 8;
    // And possibly some more
}

可以展开:

#define UNROLL (i) \
    a+=(ptr[i]) << 8; \
    a-=(ptr[i-k]) << 8;


for(int i=0; i<32; i++)
{
    UNROLL(i);
    UNROLL(i+1);
    UNROLL(i+2);
    UNROLL(i+3);
    UNROLL(i+4);
    UNROLL(i+5);
    UNROLL(i+6);
    UNROLL(i+7);
}

在一个不相关的注释上,但仍然有点相关,如果你真的想在指令计数方面获胜,请确保所有常量在代码中尽可能不那么直接地统一,这样你就不会得到以下结果组件:

// Bad
MOV r1, 4
//  ...
ADD r2, r2, 1
//  ...
ADD r2, r2, 4

而不是:

// Better
ADD r2, r2, 8

通常情况下,严肃的编译器会保护您免受此类事件的侵害,但并非所有人都会这样做。保持那些'#define','enum'和'static const'方便,并非所有编译器都会优化本地'const'变量。

答案 6 :(得分:1)

基本上,展开是循环结构的有用成本,是循环体的重要部分。大多数循环的结构(以及几乎所有可以展开的循环)包括(a)递增整数,(b)将它与另一个整数进行比较,以及(c)跳跃 - 其中两个几乎是最快的CPU的说明。因此,在几乎任何环路中,身体都会对结构进行称重,从而产生微不足道的收益。如果你的身体甚至有一个函数调用,那么身体将比结构慢一个数量级 - 你永远不会注意到这一点。

几乎唯一可以从展开中获益的东西就像memcpy(),其中循环体只是将一个字节从一个字节移动到另一个 - 这就是为什么许多C&amp;在过去十年中,C ++编译器一直在自动内联和展开memcpy。

答案 7 :(得分:0)

手动循环展开通常仅对最琐碎的循环有用。

作为参考,g ++中的C ++标准库在整个源代码中完全展开了两个循环,它实现了带有和不带谓词的'find'函数,它们看起来像:

while(first != last && !(*first == val))
  ++first;

我看了这些,以及其他循环,并且只决定了循环这个微不足道的值得做。

当然,最好的答案是只展开那些你的探查器显示它有用的循环!

答案 8 :(得分:0)

如果你已经完成了其他所有可能的事情,而这是你剩下的热点,并且循环中几乎没有任何东西,那么展开是有道理的。这些都是很多“ifs”。要验证这是否是您的最后一个选项,try this

答案 9 :(得分:0)

根据我的经验循环展开可以带来20%到50%的性能,而不会在我的intel i7 cpu上使用SEE。

对于单一操作的简单循环,在循环中存在一个条件跳转和一个增量的开销。每次跳跃和增量进行多次操作可能是有效的。有效循环展开的示例如下:

在下面没有展开的代码中,每个sum操作有一个比较+一个jumb +一个增量的开销。此外,所有操作都必须等待先前操作的结果。

template<class TData,class TSum>
inline TSum SumV(const TData* pVec, int nCount)
{
   const TData* pEndOfVec = pVec + nCount;
   TSum   nAccum = 0;

   while(pVec < pEndOfVec)
   {
       nAccum += (TSum)(*pVec++);
   }
   return nAccum;
}

在unwinded代码中,有一个比较+一个jumb +每四个sum运算一个增量的开销。此外,还有许多操作不需要等待前一操作的结果,并且可以通过编译器进行更好的优化。

template<class TData,class TSum>
inline TSum SumV(const TData* pVec, int nCount)
{
  const TData* pEndOfVec = pVec + nCount;
  TSum   nAccum = 0;

  int nCount4 = nCount - nCount % 4;
  const TData* pEndOfVec4 = pVec + nCount4;
  while (pVec < pEndOfVec4)
  {
      TSum val1 = (TSum)(pVec[0]);
      TSum val2 = (TSum)(pVec[1]);
      TSum val3 = (TSum)(pVec[2]);
      TSum val4 = (TSum)(pVec[3]);
      nAccum += val1 + val2 + val3 + val4;
      pVec += 4;
  }      

  while(pVec < pEndOfVec)
  {
      nAccum += (TSum)(*pVec++);
  }
  return nAccum;
}