23

在 C 中,我有一项任务,我必须使用分配为二维数组(数组的数组)的巨大矩阵进行乘法、求逆、移位、加法等。

我找到了 gcc 标志-funroll-all-loops。如果我理解正确,这将自动展开所有循环,而无需程序员的任何努力。

我的问题:

a) gcc 是否包括这种带有各种优化标志等的-O1优化 -O2

b)我是否必须pragma在我的代码中使用任何 s 来利用循环展开或循环自动识别?

c)如果展开可以提高性能,为什么默认不启用此选项?

d)有哪些推荐的 gcc 优化标志以尽可能最好地编译程序?(我必须运行这个针对单个 CPU 系列优化的程序,这与我编译代码的机器相同,实际上我使用march=native-O2标志)

编辑

似乎在使用 unroll 方面存在争议,在某些情况下可能会降低性能。在我的情况下,有多种方法可以在 2 个嵌套循环中进行简单的数学运算,以迭代为大量元素完成的矩阵元素。在这种情况下,展开如何会减慢或提高性能?

4

3 回答 3

28

为什么展开循环?

现代处理器流水线指令。他们喜欢知道接下来会发生什么,并根据指令执行顺序的假设进行各种花哨的优化。

但是,在循环结束时,有两种可能性!要么回到顶部,要么继续。处理器对将要发生的事情做出有根据的猜测。如果它做对了,一切都很好。如果没有,它必须在准备采用另一个分支时刷新管道并暂停一段时间。

正如您可以想象的那样,展开循环消除了分支和这些停顿的可能性,特别是在可能性与猜测相反的情况下。

想象一个代码循环执行 3 次,然后继续。如果您假设(就像处理器可能会那样)最后您将重复循环。2/3 的时候,你是对的!但是,有 1/3 的时间,你会停下来。

另一方面,想象同样的情况,但代码循环了 3000 次。在这里,展开可能只有 1/3000 的时间。

为什么展开循环?

上面提到的部分处理器奇思妙想涉及将指令从内存中的可执行文件加载到处理器的板载指令缓存(简称为 I-cache)中。这包含有限数量的指令,可以快速访问,但是当需要从内存中加载新指令时可能会停止。

让我们回到前面的例子。假设循环内相当少量的代码占用了nI-cache 字节。如果我们展开循环,它现在占用了n * 3字节。更多一点,但它可能适合单个高速缓存行,因此您的高速缓存将以最佳方式工作,而无需停止从主内存读取。

然而,3000 循环展开以使用n * 3000大量字节的 I-cache。这将需要从内存中读取几次,并且可能会将程序中其他地方的一些其他有用的东西从 I-cache 中推出。

那我该怎么办?

如您所见,展开为较短的循环提供了更多好处,但如果您打算循环大量次,最终会降低性能。

通常,智能编译器会正确猜测要展开哪些循环,但如果您确定自己知道得更多,则可以强制它。如何更好地了解?唯一的方法是尝试两种方式并比较时间!

过早的优化是万恶之源——Donald Knuth

先分析,再优化。

于 2014-06-13T03:26:53.430 回答
9

如果编译器无法在编译时预测循环的确切迭代次数(或至少预测上限,然后根据需要跳过尽可能多的迭代),则循环展开不起作用。这意味着如果您的矩阵大小是可变的,则该标志将无效。

现在回答你的问题:

a) gcc 是否包括这种带有各种优化标志的优化,如 -O1、-O2 等?

不,您必须明确设置它,因为它可能会或可能不会使代码运行得更快,并且通常会使可执行文件更大。

b) 我是否必须在我的代码中使用任何编译指示来利用循环展开或循环自动识别?

没有语用。-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

于 2014-06-13T03:57:23.047 回答
4

你得到了关于这个问题的理论背景,它留下了足够的空间来猜测你在实际运行中得到了什么。据说该选项并不总是提高性能,因为它取决于多种因素,例如循环实现、其负载/主体等。

每个代码都是不同的,如果您有兴趣找到更好的性能解决方案,最好运行两个变体,测量它们的执行时间并进行比较。

在下面的答案中查看这种方法,以了解时间测量。简而言之,您只需将代码包装到循环中,这将使您的程序运行需要几秒钟。当您自己优化循环时,最好编写一个 shell 脚本,它可以多次运行您的应用程序。

于 2014-06-13T01:15:01.047 回答