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

我一直试图通过循环展开来优化一些极其关键的性能问题代码(在蒙特卡洛模拟中被称为数百万次的快速sortingalgorithm)。 这里是我试图加速的内部循环:

// 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 } 

这完全没有区别,所以我把它改回到更易读的forms。 其他时候我也有类似的经历,我试过循环展开。 鉴于现代硬件上的分支预测器的质量,何时循环展开仍然是一个有用的优化?

循环展开是有意义的,如果你可以打破依赖链。 这给失序或超标量CPU提供了更好的时间安排和更快的运行的可能性。

一个简单的例子:

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

这里参数的依赖链非常短。 如果因为数据arrays上存在caching未命中而导致暂停,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; 

可以跑得更快。 如果在一次计算中出现caching未命中或其他暂停,则还有三个依赖链不依赖于暂停。 一个无序的CPU可以执行这些。

那些没有什么区别,因为你做了相同数量的比较。 这是一个更好的例子。 代替:

 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 ,编译器会为你优化它,无论哪个最好。

基本上,再次猜测你的编译器就不那么需要了。

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

找出你的编译器为你做了多less优化是值得的。

我发现菲利克斯·冯·莱特纳(Felix von Leitner)的演讲非常有启发性。 我build议你阅读它。 总结:现代编译器非常聪明,所以手优化几乎是无效的。

据我了解,现代编译器已经适当地展开循环 – 一个例子是gcc,如果通过优化标志它手册说:

展开循环的迭代次数可以在编译时或进入循环时确定。

所以,在实践中,你的编译器可能会为你做些微不足道的事情。 因此,要确保尽可能多的循环对于编译器来说很容易确定需要多less迭代。

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

尝试不知道是不是这样做。
这种sorting占据了很高的总体时间吗?

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

以下是如何获得最佳性能的示例。

循环展开可以在特定情况下有所帮助。 唯一的收获是不跳过一些testing!

它可以例如允许标量replace,有效地插入软件预取……你会惊讶其实际上有多有用(即使在-O3的情况下,你也可以在大多数循环中轻松地获得10%的加速)。

正如之前所说,循环取决于很多,编译器和实验是必要的。 制定一个规则是很难的(或者展开的编译器启发式是完美的)

循环展开完全取决于你的问题的大小。 这完全取决于你的algorithm能够将大小缩小为更小的工作组。 你在上面做的不是那样的。 我不确定蒙特卡洛仿真是否可以展开。

循环展开的好场景是旋转图像。 因为你可以旋转单独的工作组。 为了使这个工作,你将不得不减less迭代次数。

如果在循环中和循环中都有很多局部variables,循环展开仍然有用。 重复使用这些寄存器,而不是为循环索引保存一个寄存器。

在你的例子中,你使用less量的局部variables,而不是过度使用寄存器。

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

循环展开有助于提高CPU对分支预测的意识,但这些仍然会发生。