优化编译器如何决定何时展开循环以及展开循环的程度?

Posted

技术标签:

【中文标题】优化编译器如何决定何时展开循环以及展开循环的程度?【英文标题】:How do optimizing compilers decide when and how much to unroll a loop? 【发布时间】:2011-12-03 05:20:04 【问题描述】:

当编译器执行循环展开优化时,它如何确定展开循环或是否展开整个循环?由于这是空间性能的权衡,平均而言,这种优化技术在使程序性能更好方面的效果如何?另外,建议在什么条件下使用这种技术(即某些操作或计算)?

这不必特定于某个编译器。它可以是任何解释,概述该技术背后的想法以及在实践中观察到的情况。

【问题讨论】:

您在寻找关于编译器优化分析的论文吗? :) 我想补充一点:为什么 gcc 的帮助信息说 -funroll-all-loops 实际上会使程序运行得更慢?引用:“执行循环展开的优化。这是针对所有循环完成的,通常会使程序运行得更慢。” @Radu:以“how do it”和“under what conditions”开头的子句是疑问句。 一个很好的启发式方法是,如果超出指令交错的程度以避免数据依赖停顿和/或矢量化发挥作用,循环展开可能是一个坏主意。 @R.. D:你可以用这个来回答,但尽量让它更清楚;) 【参考方案1】:

当编译器执行循环展开优化时,它如何确定展开循环的因素或是否展开整个循环。

堆栈消耗和局部性。指令计数。基于展开和内联程序进行/传播优化的能力。循环大小是固定的,还是预期在某个范围内。配置文件输入(如果适用)。可以从循环体中删除的操作。等等

由于这是平均空间性能权衡,这种优化技术在使程序性能更好方面的效果如何?

这在很大程度上取决于输入(您的程序)。它可能会更慢(不典型),也可能会快几倍。编写一个程序以优化运行,并使优化器能够完成其工作是学习的。

另外,建议在什么条件下使用这种技术(即某些操作或计算)

通常,在非常小的实体上进行大量迭代,尤其是那些无分支且具有良好数据局部性的实体。

如果您想知道该选项是否对您的应用、个人资料有帮助。

如果你需要更多,你应该预留一些时间来学习如何编写最佳程序,因为这个主题相当复杂。

【讨论】:

您对编写最佳程序的资源有什么建议吗? 这真的取决于你目前的知识水平和你编写的程序......也许你会发现这是一个很好的资源:@​​987654321@ +1 对于贾斯汀的链接。发现 MASM 论坛上的这句话非常苛刻:“胆小的人不可以。如果 MASM 超出您的范围,请使用服务器端脚本。” @John 是的 - Anger 的手册提供了 很多 的有用信息。我已经编写了相当多的性能关键程序,并且很少求助于编写程序集。我通常会求助于元编程实现(我喜欢可移植性)。感谢您的报价:)【参考方案2】:

简单的分析是计算指令数 - 一个 2 指令循环展开 10 次 有 11 条指令而不是 20 条产生 11/20 的加速。但是对于现代处理器架构,它要复杂得多。取决于缓存大小和处理器指令流水线的特性。上述示例的运行速度可能会快 10 倍而不是 2 倍。展开 1000x 而不是 10x 也可能会运行得更慢。如果不针对特定的处理器,编译器(或您为它们编写的 pragma)只是猜测。

【讨论】:

【参考方案3】:

什么时候(在我看来)展开循环比较好:

循环很短,可能所有使用的变量都在处理器寄存器中。展开后变量是“重复的”,但仍在寄存器中,因此没有内存(或缓存)损失。

循环(循环展开号未知)将至少执行几次或几十次,因此有理由将展开的整个循环加载到指令缓存中。

如果循环很短(一个或几个指令),它可能对展开非常有益,因为用于确定是否应该再次执行的代码执行频率较低。

【讨论】:

【参考方案4】:

好的,首先,我不知道编译器是如何自动执行此操作的。而且我很确定编译器必须从中选择至少 10 种算法,如果不是 100 种算法的话。 无论如何,它可能是特定于编译器的。

但是,我可以帮你计算它的有效性。

请注意,这种技术通常不会给您带来很大的性能提升。 但在重复循环计算中并能给出很高的百分比性能。 这是因为循环内的函数通常比循环的条件检查花费更多的计算时间。

所以,假设我们有一个带有常量的简单循环,因为您懒得进行复制粘贴,或者只是认为它看起来会更好:

for (int i = 0; i < 5; i++)

    DoSomething();

这里有 5 个 int 比较、5 个增量和 5 个 DoSomethig() 调用。 因此,如果 DoSomething() 相对较快,那么我们进行了 15 次操作。 现在,如果你展开这个,你会减少到只有 5 个操作:

DoSomething();
DoSomething();
DoSomething();
DoSomething();
DoSomething();

现在使用常量更容易,让我们看看它如何使用变量:

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

    DoSomething();

这里有 n 个 int 比较、n 个增量和 n DoSomethig() 调用 = 3n。 现在,我们不能完全展开它,但我们可以通过一个常数因子展开它(预计 n 越高,我们应该展开越多):

int i;
for (i = 0; i < n; i = i+3)

    DoSomething();
    DoSomething();
    DoSomething();

if (i - n == 2)

    DoSomething(); // We passed n by to, so there's one more left

else if (i - n == 1)

    DoSomething();  //We passed n by only 1, so there's two more left
    DoSomething();

现在我们有 n/3+2 个 int 比较、n/3 个增量和 n 个 DoSomethig() 调用= (1 2/3)*n。 我们为自己节省了 (1 1/3)*n 次操作。这将计算时间几乎减少了一半。

仅供参考,另一种巧妙的展开技术称为 Duff's device。 但它是非常特定于编译器和语言实现的。有些语言实际上会更糟。

【讨论】:

以上是关于优化编译器如何决定何时展开循环以及展开循环的程度?的主要内容,如果未能解决你的问题,请参考以下文章

循环展开与循环平铺

OpenCL Unrolling Loops优化

循环展开有利的条件以及收益率下降的点?

循环展开和优化

循环展开对内存绑定数据的影响

为啥 clang 无法展开循环(即 gcc 展开)?