GCC循环展开标志真的有效吗?

25
在C语言中,我必须使用分配为二维数组(数组的数组)的巨大矩阵执行乘法、求逆、转置、加法等操作。我发现gcc标志“-funroll-all-loops”。如果我理解正确,这将自动展开所有循环,程序员不需要作任何工作。
我的问题是:
a) gcc是否会通过各种优化标志(如-O1、-O2等)包括这种优化?
b) 我是否需要在代码中使用pragma来充分利用循环展开,或者是循环被自动识别?
c) 如果展开循环可以提高性能,为什么不将此选项设置为默认选项?
d) 推荐使用哪些gcc优化标志以最佳方式编译程序?(我必须为单个CPU家族运行此程序,并且与我编译代码的机器相同,实际上我使用march=native和-O2标志)
编辑
似乎有争议关于使用展开可能会减慢性能的情况。在我的情况下,有各种方法可以在2个嵌套的for循环中执行简单的数学运算,以迭代大量元素的矩阵。在这种情况下,循环展开如何减慢或增加性能?

6
为什么这个选项不默认启用,如果展开循环可以提高性能?从文档中可以看到:funroll-all-loops: ...这通常会使程序运行更慢。它可能导致指令缓存未命中,并且代码大小将增加。它不是一项自动的好处。 - Ed S.
此外,循环展开并不总是能提高性能。 - Mysticial
回答问题1,根据Ed在文档中提到的内容,没有任何-O选项会添加-funroll-loops-funroll-all-loops - IllusiveBrian
展开循环何时有用,何时会降低性能? - AndreaF
考虑使用单一的malloc。也就是说,使用作为2D访问的1D数组。这将利用行访问之间的局部性。其次,考虑使用循环瓦片或手动展开循环。实验以查看哪些参数更适合缓存、寄存器和功能单元。需要进行分析以观察最终效果。如果您懒得做,可以通过启用分析和循环展开来让gcc决定。 - kchoi
显示剩余2条评论
3个回答

32

为什么要展开循环?

现代处理器会对指令进行流水线处理。它们喜欢知道下一步要做什么,并根据指令执行的顺序进行各种高级优化。

但在循环结束时,有两种可能性!要么返回到循环的开头,要么继续执行。处理器会根据经验猜测下一步会怎样。如果猜对了,就万事大吉。如果猜错了,就需要清空流水线并暂停一段时间,以准备执行另一个分支。

可以想象,展开循环可以消除分支和潜在的暂停,特别是在猜测错误的情况下。

假设有一个代码循环执行3次,然后继续执行。如果你(和处理器)认为你将重复循环,那么有2/3的时间你是正确的!但还有1/3的时间需要暂停等待。

另一方面,假设相同的代码循环执行3000次。这里,通过展开循环获得收益的概率可能只有1/3000。

为什么要展开循环?

上述处理器优化中的一部分涉及将可执行文件中的指令加载到处理器的指令高速缓存(简称I-cache)中。I-cache中可以快速访问有限数量的指令,但在需要从内存中加载新指令时可能会暂停。

回到前面的例子。假设循环中的代码量占用n字节的I-cache。如果我们展开循环,则现在占用了n*3字节。虽然多了一点,但它很可能适合一个缓存行,因此您的缓存将始终保持最佳状态,不需要暂停从主存中读取数据。

然而,对于有3000个循环的情况,展开后将使用庞大的n*3000字节的I-cache。这将导致多次从内存中读取数据,并且可能会将程序中其他有用的内容从I-cache中推出。

那我该怎么做?

正如你所看到的,展开循环对于较短的循环提供了更多的好处,但如果你想要循环大量的次数,就会损害性能。

通常,聪明的编译器会猜测哪些循环应该被展开,但如果你确定自己知道更好的方法,可以强制让编译器这样做。如何才能更好地知道呢?唯一的方法是尝试两种方式并比较计时!

过早优化是万恶之源——唐纳德·科努斯

先进行分析,再进行优化。


1
那么,您可能不建议在除了已经确定从中受益的编译单元之外的任何地方使用“-funroll-loops”,如果有的话? - SamB
降低评分是因为展开循环不同于重复数千次的代码。可以参考Duff设备获取更好的示例。 - Euri Pinhollow
1
-funroll-loops 在编译时可以确定迭代次数的循环进行展开,或者在进入循环时确定。 - Euri Pinhollow
1
这并不是展开循环的好处的准确描述。展开通常不会“消除”分支,因为展开的循环仍然有一个分支,并且它通常不能帮助分支预测:任何适度大量的迭代都将具有相同的预测行为:正确预测取(回到顶部)然后在退出时错误预测一次。对于_小_迭代计数,循环展开通常会导致更差的预测,因为您需要为奇数迭代再添加另一个“尾处理”部分,这也可能会出现错误预测。 - BeeOnRope
3
循环展开的主要优点是:(1) 减少与循环结束检查和循环计数器变量相关的开销;(2) 当循环的几个迭代可以作为一个整体进行优化时获得的效率。 - BeeOnRope

10
循环展开在编译器无法在编译时准确预测循环迭代次数(或者至少预测一个上限并跳过多余的迭代)时无法使用(这意味着如果您的矩阵大小是可变的,则该标志将不起作用)。
现在回答您的问题:
a)gcc是否包括此类优化,如-O1、-O2等各种优化标志?
不包括,您必须显式设置它,因为它可能会使代码运行更快,但通常会使可执行文件变得更大。
b)我是否需要在我的代码中使用任何pragma来利用循环展开,或者循环会自动识别?
没有pragma。通过-funroll-loops,编译器启发式地决定要展开哪些循环。如果您想强制展开,则可以使用-funroll-all-loops,但通常会使代码运行更慢。
c)如果展开循环可以提高性能,为什么不默认启用此选项?
它并不总是能够提高性能!此外,并非所有事情都是关于性能的。有些人实际上关心具有小内存的小型可执行文件(参见:嵌入式系统)。
d)编译程序的最佳gcc优化标志是什么?(我必须对单个CPU系列进行优化,该系列与我编译代码的机器相同,实际上我使用march = native和-O2标志)
没有万能的解决方案。您需要思考、测试和观察。实际上,有一个定理表明永远不可能存在完美的编译器。
您对程序进行了分析吗?在这些事情中,分析是非常有用的技能。
来源(大部分):https://gcc.gnu.org/onlinedocs/gcc-3.4.4/gcc/Optimize-Options.html

3
你对“循环展开”有错误的定义。循环展开不仅仅是指将循环体重复N次以使得循环完全消失,而是指将循环体重复2、4或更一般地M次,然后减少循环次数(因子为M),但它仍然是一个循环。前一种技术可以称为“完全展开”,但通常情况下你没有编译时常量的循环次数,所以这个术语并不适用。 - BeeOnRope

4
您正在了解有关该问题的理论背景,这留下了足够的空间来猜测在实际运行中会得到什么。据说该选项并不总是增加性能,因为它取决于各种因素,例如循环实现、其负载/体和其他因素。
每个代码都是不同的,如果您有兴趣找到更好的性能解决方案,最好的方法就是运行两个变体,测量它们的执行时间并进行比较。
请查看下面答案中的方法,以了解时间测量的想法。简而言之,您只需将代码包装到循环中,这将导致程序运行需要几秒钟。由于您正在优化循环本身,所以编写一个运行应用程序多次的shell脚本是个好主意。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接