什么时候,如果循环展开仍然有用?

时间:2010-02-27 22:41:12

标签: performance language-agnostic optimization micro-optimization

我一直在尝试通过循环展开来优化一些极其性能关键的代码(一种快速排序算法,在蒙特卡罗模拟中被称为数百万次)。这是我试图加速的内循环:

// Search for elements to swap.
while(myArray[++index1] < pivot) {}
while(pivot < myArray[--index2]) {}

我尝试展开类似:

while(true) {
    if(myArray[++index1] < pivot) break;
    if(myArray[++index1] < pivot) break;
    // More unrolling
}


while(true) {
    if(pivot < myArray[--index2]) break;
    if(pivot < myArray[--index2]) break;
    // More unrolling
}

这完全没有区别所以我把它改回了更易读的形式。我曾经尝试过循环展开,但我有类似的经历。鉴于现代硬件上的分支预测器的质量,何时(如果有的话)循环展开仍然是一个有用的优化?

9 个答案:

答案 0 :(得分:106)

如果你可以打破依赖链,循环展开是有意义的。这使得无序或超标量CPU可以更好地安排事情并因此运行得更快。

一个简单的例子:

for (int i=0; i<n; i++)
{
  sum += data[i];
}

这里参数的依赖链非常短。如果因为数据阵列上有缓存未命中而导致停顿,那么cpu除了等待之外什么也做不了。

另一方面,这段代码:

for (int i=0; i<n; i+=4)
{
  sum1 += data[i+0];
  sum2 += data[i+1];
  sum3 += data[i+2];
  sum4 += data[i+3];
}
sum = sum1 + sum2 + sum3 + sum4;

可以跑得更快。如果在一次计算中遇到缓存未命中或其他停顿,则仍有三个其他依赖链不依赖于停顿。乱序CPU可以执行这些。

答案 1 :(得分:21)

那些没有任何区别,因为你正在进行相同数量的比较。这是一个更好的例子。而不是:

for (int i=0; i<200; i++) {
  doStuff();
}

写:

for (int i=0; i<50; i++) {
  doStuff();
  doStuff();
  doStuff();
  doStuff();
}

即便如此,它几乎肯定无关紧要,但你现在正在进行50次比较而不是200次(想象一下比较更复杂)。

手动循环展开通常在很大程度上是历史的工件。这是一个很好的编译器会在重要的时候为你做的事情中的另一个。例如,大多数人都不愿意写x <<= 1x += x而不是x *= 2。你只需编写x *= 2,编译器就会为你优化它,无论什么是最好的。

基本上,对猜测编译器的猜测越来越少。

答案 2 :(得分:14)

无论现代硬件上的分支预测如何,大多数编译器都会为您循环展开。

了解编译器为您做了多少优化是值得的。

我发现Felix von Leitner's presentation在这个问题上非常有启发性。我建议你阅读它。简介:现代编译器非常聪明,因此手动优化几乎无效。

答案 3 :(得分:2)

据我所知,现代编译器已经在适当的时候展开循环 - 一个例子是gcc,如果传递了优化标记,那么手册说它会:

  

展开其编号为的循环   迭代可以确定   编译时或进入   循环。

因此,在实践中,您的编译器很可能会为您做一些简单的案例。因此,您需要确保尽可能多的循环使编译器能够确定需要多少次迭代。

答案 4 :(得分:2)

循环展开,无论是手动展开还是编译器展开,往往会适得其反,特别是对于最新的x86 CPU(Core 2,Core i7)。结论:在您计划部署此代码的任何CPU上,使用和不使用循环对您的代码进行基准测试。

答案 5 :(得分:1)

不知道的尝试不是这样做的方式 这种情况会占整个时间的很大比例吗?

所有循环展开都会减少递增/递减的循环开销,比较停止条件和跳转。如果你在循环中所做的事情比循环开销本身需要更多的指令周期,那么你不会看到很多改进百分比。

Here's an example of how to get maximum performance.

答案 6 :(得分:1)

循环展开在特定情况下可能会有所帮助。唯一的好处是不会跳过一些测试!

它可以例如允许标量替换,有效插入软件预取......实际上你会感到惊讶(通过积极展开,它可以很容易地在大多数循环中获得10%的加速,即使使用-O3)。 / p>

如前所述,它在很大程度上取决于循环,编译器和实验是必要的。制定规则很难(或者展开的编译器启发式是完美的)

答案 7 :(得分:0)

循环展开完全取决于您的问题大小。它完全取决于您的算法能够将大小缩小为较小的工作组。你上面做的不是那样的。我不确定蒙特卡罗模拟是否可以展开。

循环展开的好方案是旋转图像。因为您可以旋转单独的工作组。要使其工作,您必须减少迭代次数。

答案 8 :(得分:0)

如果循环内部和循环中存在大量局部变量,则循环展开仍然有用。要重用这些寄存器而不是为循环索引保存一个。

在您的示例中,您使用少量局部变量,而不是过度使用寄存器。

比较(到循环结束)也是一个主要的缺点,如果比较很重(即非test指令),特别是如果它依赖于外部函数。

循环展开有助于提高CPU对分支预测的意识,但无论如何都会发生。