循环提升仍然是C代码的有效手动优化吗?

使用最新的gcc编译器,我还需要考虑这些类型的手动循环优化,还是编译器会为我做好充分的处理?

如果你的探查器告诉你循环有问题,那么,需要注意的是循环中的内存引用, 知道它在循环中是不变的,但编译器没有。 这是一个人为的例子,将一个元素冒泡到数组的末尾:

for ( ; i < a->length - 1; i++) swap_elements(a, i, i+1); 

可能知道对swap_elements的调用不会更改a->length的值,但如果swap_elements的定义在另一个源文件中,则很可能编译器没有。 因此,在循环中提升a->length的计算是值得的:

 int n = a->length; for ( ; i < n - 1; i++) swap_elements(a, i, i+1); 

在性能关键的内循环中,我的学生可以通过像这样的转换获得可测量的加速。

注意,没有必要提升n-1的计算; 任何优化编译器都能完全发现局部变量之间的循环不变计算。 它的内存引用和函数调用可能更难。 并且具有n-1的代码更明显正确。

正如其他人所指出的那样,在你进行了分析并发现循环是一个真正重要的性能瓶颈之前,你没有做任何事情。

编写代码,对其进行概要分析,只考虑在找到不够快的东西时进行优化,并且您无法想到可以减少/避免瓶颈的替代算法。

对于现代编译器,这个建议更为重要 – 如果您编写简单的干净代码,编译器的优化器通常可以更好地优化代码,而不是尝试为其提供时髦的“预优化”代码。

检查生成的组件并亲自查看。 查看循环不变代码的计算是在循环内部还是在编译器生成的汇编代码中的循环外部完成的。 如果没有进行环路吊装,请自行吊装。

但正如其他人所说 ,你应该首先找到你的瓶颈。 一旦你确定这实际上是一个瓶颈,那么你应该检查编译器是否在热点中执行循环提升(也就是循环不变代码运动)。 如果不是,请帮助它。

编译器通常在这种类型的优化中表现出色,但他们确实错过了一些情况。 一般来说,我的建议是:将代码编写为尽可能可读(这可能意味着你提升循环不变量 – 我更喜欢读取那种方式编写的代码),如果编译器错过了优化,则提出错误来帮助修复编译器。 如果您有一个不能等待编译器修复的硬性能要求,或者编译器编写者告诉您他们无法解决问题,那么只将优化放入源代码中。

如果它们可能对性能很重要,那么你仍然需要考虑它们。

当被提升的值需要大量的工作来计算时,环路提升是最有益的。 如果计算需要大量工作,那可能是一个不合时宜的调用。 如果它是一个脱机的呼叫,那么最新版本的gcc不太可能发现它每次都会返回相同的值。

有时候人们会先告诉你。 他们并不是真正的意思,他们只是认为,如果你足够聪明,可以弄清楚什么时候值得担心性能,那么你就足够聪明地忽略了他们的经验法则。 显然,无论您是否有异形,以下代码都可能“过早优化”:

 #include  bool isPrime(int p) { for (int i = 2; i*i <= p; ++i) { if ((p % i) == 0) return false; } return true; } int countPrimesLessThan(int max) { int count = 0; for (int i = 2; i < max; ++i) { if (isPrime(i)) ++count; } return count; } int main() { for (int i = 0; i < 10; ++i) { std::cout << "The number of primes less than 1 million is: "; std::cout << countPrimesLessThan(1000*1000); std::cout << std::endl; } } 

它需要一种“特殊”的方法来进行软件开发,而不是手动将对countPrimesLessThan的调用提升到循环之外,无论你是否已经分析过。

只有在其他方面(如可读性,意图清晰度或结构)受到负面影响时,早期优化才是糟糕的。

无论如何你必须声明它,循环提升甚至可以提高清晰度,它明确地记录了你的假设“这个值不会改变”。

根据经验,我不会提升std :: vector的count / end迭代器,因为它是一个容易优化的常见场景。 我不会提升任何我可以相信我的优化器提升的东西,并且我不会提升任何已知不重要的东西 – 例如当通过十几个窗口列表来响应按钮点击时。 即使它需要50毫秒,它仍然会对用户显得“不稳定”。 (但即使这是一个危险的假设:如果一个新function需要在同一代码上循环20次,它会突然变慢)。 您仍然应该提升操作,例如打开要附加的文件句柄等。

在许多情况下 – 循环提升非常好 – 考虑相对成本有很大帮助:提升计算的成本与穿过身体的成本相比是多少?


至于一般的优化,在很多情况下,分析器没有帮助。 代码可能具有非常不同的行为,具体取决于调用路径。 图书馆作者通常不知道他们的呼叫路径otr频率。 隔离一段代码以使事物具有可比性已经可以显着改变行为。 分析器可能会告诉你“循环X很慢” ,但它不会告诉你“循环X很慢,因为调用Y正在为其他人颠倒缓存” 。 一个分析器无法告诉你“这个代码很快,因为你的CPU很笨拙,但在Steve的计算机上会很慢”。


一个好的经验法则通常是编译器执行它能够进行的优化。 优化是否需要有关代码的任何知识,而编译器并不是很明显? 然后编译器很难自动应用优化,你可能想自己做

在大多数情况下,垂直提升是一个全自动的过程,不需要高级的代码知识 – 只需要大量的生命周期和依赖性分析,这是编译器首先擅长的。

可以编写代码,编译器无法确定是否可以安全地提升某些东西 – 在这种情况下,您可能希望自己完成,因为这是一种非常有效的优化。

举个例子,拿Steve Jessop发布的片段:

 for (int i = 0; i < 10; ++i) { std::cout << "The number of primes less than 1 billion is: "; std::cout << countPrimesLessThan(1000*1000*1000); std::cout << std::endl; } 

提升countPrimesLessThan的调用是否安全? 这取决于定义函数的方式和位置。 如果它有副作用怎么办? 它是否被调用一次或十次,以及何时被调用可能会产生重要的区别。 如果我们不知道函数是如何定义的,我们就不能将它移到循环之外。 如果编译器要执行优化,情况也是如此。

函数定义对编译器是否可见? function是否足够短,我们可以信任编译器内联它,或者至少分析副作用的function? 如果是这样,那么是的,它将在循环外提升它。

如果定义不可见,或者函数非常庞大和复杂,那么编译器可能会认为函数调用无法安全移动,然后它不会自动将其提升。

记住80-20规则。(80%的执行时间花费在程序中20%的关键代码上)优化代码没有任何意义,这对程序的整体效率没有显着影响。

人们不应该为代码中的这种局部优化而烦恼。所以最好的方法是对代码进行分析,以找出程序中关键部分,这些部分消耗大量CPU周期并尝试对其进行优化。这种优化将真正使得某种意义,将导致提高计划效率。