【问题标题】:Is GCC loop unrolling flag really effective?GCC 循环展开标志真的有效吗?
【发布时间】:2014-06-13 00:40:46
【问题描述】:

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

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

我的问题:

a) gcc 是否包含这种优化,以及 -O1-O2 等各种优化标志?

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

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

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

编辑

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

【问题讨论】:

  • "如果展开提高性能,为什么默认不启用此选项?" - 来自文档:funroll-all-loops: ...This usually makes programs run more slowly.。您可以命中指令缓存未命中,并且您的代码大小会增加。这不是一个自动的好处。
  • 另外,循环展开并不总能提高性能。
  • 回答 1,根据文档 Ed 没有提到任何 -O 选项添加 -funroll-loops-funroll-all-loops
  • 那么什么时候展开循环有用,什么时候会减慢性能?
  • 基本上所有操作都是在双嵌套for循环循环中完成的,以迭代矩阵元素,对每个元素进行简单的数学运算,在这种情况下我不知道为什么应该或不应该提高性能.

标签: c gcc gcc4.8


【解决方案1】:

为什么要展开循环?

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

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

正如您所想象的,展开循环可以消除分支和这些停滞的可能性,尤其是在可能性与猜测相反的情况下。

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

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

为什么展开循环?

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

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

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

那我该怎么办?

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

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

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

先配置,后优化。

【讨论】:

  • 那么,您大概不建议在任何东西上使用-funroll-loops,除非是专门确定从中受益的编译单元,如果有的话?
  • 投反对票,因为展开循环与重复代码数千次不同。有关更好的示例,请参阅 Duff 设备。
  • "-funroll-loops 展开循环,其迭代次数可以在编译时确定or upon entry to the loop"
  • 这不是展开循环的好处的准确描述。展开通常不会“消除”分支,因为展开的循环仍然有一个分支,并且它通常对分支预测没有帮助:任何中等数量的迭代都将具有相同的预测行为:正确预测采取(循环回到顶部),然后在退出时预测错误一次。对于 small 迭代计数,循环展开通常会导致 worse 预测,因为您需要为奇数迭代设置另一个“尾部处理”部分,这也可能导致错误预测。
  • 循环展开的主要好处是:(1) 减少与循环结束检查和循环计数器变量相关的开销,以及 (2) 循环多次迭代时获得的效率整体优化。
【解决方案2】:

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

现在回答你的问题:

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

【讨论】:

  • 您对“循环展开”的定义有误。循环展开并不(仅)指将循环体复制 N 次,以使循环完全消失。它也指将主体复制 2、4 或更多次,通常 M 次,然后运行循环更少次(乘以 M 的因子),但它仍然是一个循环。前一种技术可以称为“完全展开”,但它不是很适用,因为通常您没有编译时常量行程计数。
【解决方案3】:

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

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

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

【讨论】:

    猜你喜欢
    • 2016-10-24
    • 1970-01-01
    • 1970-01-01
    • 2017-01-21
    • 2010-09-16
    • 2019-09-18
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多