超越-O3/-Ofast的G++优化

95

问题

我们有一个中等规模的模拟任务程序,需要进行优化。我们已经尽力优化源代码,包括使用GprofValgrind进行分析。

最终完成后,我们希望在多个系统上运行该程序,可能会运行数月。因此,我们非常有兴趣将优化推到极限。

所有系统都将在相对较新的硬件(Intel i5或i7)上运行Debian/Linux。

使用最新版本的g++,有哪些可能的优化选项超越-O3 / -Ofast?

我们还对那些长期回报昂贵的微小优化感兴趣。

我们目前使用的方法

现在我们使用以下g ++优化选项:

  • -Ofast:最高“标准”优化级别。虽然不符合标准,但我们的计算中包含的-ffast-math没有引起任何问题,因此我们决定采用它。
  • -march=native:启用使用所有CPU特定指令。
  • -flto以允许跨不同编译单元进行链接时优化。

7
你尝试过基于程序剖析的优化吗?当然,这需要具有“代表性”的剖析数据。除此之外,我认为还可以通过找出热点代码,观察处理器生成的代码,并尝试更好地组织数据和代码或设计不同的算法来进行优化。 - Mats Petersson
8
请注意,推迟程序启动一天,并利用那一天来优化程序以获得1%的性能提升,只有在运行时间达到100天后才会收回成本。换句话说,尽早启动程序几天通常比进行小幅度的优化更为重要。 - sth
3
@sth:这当然是正确的。但是我希望能找到一些提示/技巧,以便在以后的项目中也可以重复使用,这样我就不必花费通过优化获得的时间了... - Haatschii
2
@OliCharlesworth:你可能是对的,所以我把那个显式的例子拿掉了。然而,我希望可能会有标志/技巧,可以获得甚至比小幅加速更多的效果。 - Haatschii
1
瓶颈在哪里? - user1382306
显示剩余6条评论
8个回答

115
大多数答案都提供了替代解决方案,例如不同的编译器或外部库,这很可能需要大量的重写或集成工作。我将尝试坚持问题所问的,并专注于仅使用GCC可以做什么,通过激活编译器标志或对代码进行最小更改,如OP所要求的那样。这不是一个“必须这样做”的答案,而是我已经成功尝试过的一些GCC调整的集合,如果在您特定的上下文中相关,您可以尝试一下。

关于原问题的警告

在深入讨论之前,有几个关于该问题的警告,通常是给那些读到这个问题并说“OP正在优化O3,我应该使用与他相同的标志!”的人。

  • -march=native 启用特定于给定CPU架构的指令,并且这些指令在不同架构上可能不存在。如果在不同的CPU系统上运行程序,则程序可能根本无法工作,或者速度显着降低(因为它还启用了mtune=native),因此如果您决定使用它,请注意此事项。更多信息here
  • -Ofast,如您所述,启用了一些不符合标准的优化,因此也应谨慎使用。更多信息here

尝试的其他GCC标志

不同标志的详细信息可以在这里找到。

  • -Ofast 启用 -ffast-math,进而启用 -fno-math-errno-funsafe-math-optimizations-ffinite-math-only-fno-rounding-math-fno-signaling-nans-fcx-limited-range。您可以通过有选择地添加一些额外的标志,如 -fno-signed-zeros-fno-trapping-math 等,更进一步地进行 浮点数计算优化。这些标志不包括在 -Ofast 中,但可以在计算中提供一些额外的性能提升,但您必须检查它们是否真正对您有益并且不会破坏任何计算。
  • GCC 还具有大量其他未由任何 "-O" 选项启用的 其他优化标志。它们被列为“可能产生错误代码的实验性选项”,因此应谨慎使用,并通过测试正确性和基准测试来检查它们的效果。尽管如此,我经常使用 -frename-registers,这个选项从未为我产生过不良结果,并且往往会给出明显的性能提升(即可以在基准测试中测量)。这是一种非常依赖于您的处理器类型的标志。 -funroll-loops 有时也会产生良好的结果(并且还暗示了 -frename-registers),但它取决于您的实际代码。

PGO

GCC具有基于性能分析的优化(Profile-Guided Optimisations)功能。虽然GCC没有太多关于此功能的精确文档,但是让它运行起来非常简单。

  • 首先使用-fprofile-generate编译您的程序。
  • 让程序运行(执行时间会显著变慢,因为代码还会生成.gcda文件中的性能分析信息)。
  • 使用-fprofile-use重新编译程序。如果您的应用程序是多线程的,请添加-fprofile-correction标志。

使用GCC进行PGO可以产生惊人的结果,并且真正显著提高性能(我最近参与的一个项目中看到了15-20%的速度提升)。显然,问题在于拥有一些足够代表您的应用程序执行的数据,这并不总是可用或易于获取。

GCC的并行模式

GCC具有并行模式,该模式是在GCC 4.2编译器发布时首次推出的。

基本上,它为您提供了许多C++标准库算法的并行实现。要在全局范围内启用它们,您只需要向编译器添加-fopenmp-D_GLIBCXX_PARALLEL标志。您也可以在需要时选择性地启用每个算法,但这将需要进行一些小的代码更改。

有关此并行模式的所有信息,请单击此处

如果您经常在大型数据结构上使用这些算法,并且有许多硬件线程上下文可用,这些并行实现可以大大提高性能。到目前为止,我只使用了sort的并行实现,但为了给出一个粗略的想法,我设法将排序时间从14秒降低到4秒,测试环境为:具有自定义比较器功能和8个内核的100万对象向量。

额外技巧

与前面的部分不同,这一部分需要对代码进行一些小的更改。它们也是GCC特定的(其中一些也适用于Clang),因此应使用编译时宏来保持代码在其他编译器上的可移植性。该部分包含一些更高级的技术,如果您没有一定的汇编水平理解,则不应使用。还要注意,处理器和编译器现在非常聪明,因此可能很难从这里描述的函数中获得任何显着的好处。
  • GCC内置函数,可以在这里找到。像__builtin_expect这样的构造可以通过提供分支预测信息帮助编译器进行更好的优化。其他构造,例如__builtin_prefetch将数据带入缓存以在访问之前帮助减少缓存未命中
  • 函数属性可以在这里找到。特别是,应该查看hotcold属性;前者将指示编译器函数是程序的热点,并且更积极地优化该函数,并将其置于文本部分的一个特殊子部分,以便更好地定位;后者将为大小优化函数,并将其放置在文本部分的另一个特殊子部分。

我希望这个答案能对一些开发者有所帮助,如果有任何修改或建议,我很乐意考虑。


5
谢谢,这个回答描述了我们最终所做的事情,特别是 PGO 巨大地提高了效用。此外,我也喜欢 @zaufi 提出的 ACOVEA 项目,尽管它在这个项目中没有起到作用。 - Haatschii
4
哇,我之前不知道有 PGO 选项!在我的情况下可以提高约30%的性能。 - fhucho
1
这些不包括在-Ofast中。我非常确定那是错误的。如果您查看GCC文档中的-ffast-math(由-Ofast打开),它还会打开-funsafe-math-optimizations,从而打开-fassociative-math。(等等) 文档中有一句话:“此选项未由任何-O选项打开”,我认为这是文档错误,因为-Ofast确实将它们打开。 此外,PGO打开-funroll-loops,从而打开-frename-registers。 - uLoop
@uLoop:GCC文档确实不总是清晰易懂。我已经使用编译器的“-Q”标志检查了这些标志,并相应地进行了调整。 - Pyves
1
@Pyves 我还发现了另一种方法可以与你的方法相辅相成:使用GCC和Perf进行反馈指导优化:https://blog.wnohang.net/index.php/2015/04/29/feedback-directed-optimization-with-gcc-and-perf/然而,这个方法有些问题,因为文章已经过时,一些命令已经被弃用,而gcov_create在读取perf的perf.data文件时也出现了问题。也许你可以调查一下并提供一些指导。 - Demon
显示剩余3条评论

18

相对较新的硬件(Intel i5或i7)

为什么不投资购买Intel编译器和高性能库呢?在优化方面它可以比GCC表现出更高的性能,通常能够达到10%至30%甚至更多,尤其是对于大量数字计算的程序。此外,Intel还提供了许多用于高性能数字计算(并行)应用程序的扩展和库,如果您有能力将其集成到您的代码中,可能会带来巨大的收益,因为这可能会节省数月运行时间。

我们已经尽力将源代码优化到我们的编程技能极限

根据我的经验,使用分析器进行微观和纳米级优化往往与宏观优化(简化代码结构)以及最重要但经常被忽视的内存访问优化(例如,引用局部性,顺序遍历,最小化间接寻址,消除缓存未命中等)相比,回报率较低。后者通常涉及设计内存结构以更好地反映内存的使用方式(遍历)。有时,仅需更改容器类型就可以获得巨大的性能提升。通常,使用分析器时,您会迷失在逐条指令的优化细节中,并且内存布局问题不会出现并且通常会被忽略,因此忘记查看更大的图片。这是一种更好的投资时间的方法,并且收益可能非常高(例如,许多O(logN)算法最终表现几乎与O(N)一样慢,仅仅是因为内存布局差(例如,使用链接列表或链接树是巨大性能问题的典型罪犯,而不是连续存储策略)。)


我们目前不使用英特尔编译器的原因是,它不支持我们正在使用的某些 C++11 功能。如果这种情况很快改变,我们也会尝试 ICC。我大部分同意您的第二个观点。但除了让更多人查看代码外,我不知道我们还能做什么来进一步改进它。因此,我的问题是是否有更多的事情可以让编译器实现。 - Haatschii
2
@Haatschii,是的,很抱歉我不能直接回答你的问题(即如何充分利用GCC),因为我认为你不能。我只是觉得将这些几个要点(使用ICC和进行内存优化)提出来,作为实现你目标更好的途径,值得一试。 - Mikael Persson
2
我对“通常为10%至30%甚至更多”的说法非常怀疑。至少,这些边际远远超出了我在自己的工作中测量到的范围。我很想看到一份公开发表的基准测试集合,证明在使用相同的编译器标志和已发布的标志的情况下,是否存在优化机会,即使只是为了查看我是否错过了英特尔编译器的优化机会。 - apmccartney

8

嗯,那么你可以尝试最后一件事情:

ACOVEA项目:通过进化算法分析编译器优化--正如描述的那样,它尝试使用遗传算法为您的项目选择最佳的编译器选项(多次编译和检查时间,向算法提供反馈:)--但结果可能令人印象深刻! :)


8
如果您有能力,可以尝试使用VTune。它提供比简单采样(由gprof提供,据我所知)更多的信息。您可以尝试使用Code Analyst。后者是一个不错的免费软件,但可能无法正确地工作(或完全无法工作)与英特尔CPU。
拥有这样的工具,它允许您检查各种度量,如缓存利用率(以及基本内存布局),如果充分利用,则可以大大提高效率。
当您确信自己的算法和结构是最佳的时候,您应该绝对使用i5和i7上的多个核心。换句话说,尝试使用不同的并行编程算法/模式,并查看是否可以获得加速。
当您拥有真正的并行数据(类似于数组的结构,在其中执行相似/相同的操作)时,您应该尝试使用OpenCL和SIMD指令(更易设置)。

4

关于当前所选答案的一些注释(我还没有足够的声望将其发布为评论):

答案说:

-fassociative-math-freciprocal-math-fno-signed-zeros-fno-trapping-math。这些选项不包括在 -Ofast 中,但可以在计算中提供一些额外的性能提升。

也许当答案发布时是正确的,但GCC文档表示所有这些选项都由 -funsafe-math-optimizations 启用,该选项由 -ffast-math 启用,后者由 -Ofast 启用。可以使用命令 gcc -c -Q -Ofast --help=optimizer 进行检查,该命令显示了哪些优化是由 -Ofast 启用的,并确认这些选项都已启用。

答案还说:

其他未被任何“-O”选项启用的优化标志... -frename-registers

再次强调,以上命令显示,在我的GCC 5.4.0中,-Ofast 默认启用 -frename-registers


1

1

没有更多的细节很难回答:

  • 什么类型的数字计算?
  • 你使用了哪些库?
  • 并行程度如何?

你能写下代码中最耗时的部分吗?(通常是紧密循环的部分)

如果你受限于CPU,答案将与你受限于IO时不同。

请再提供更多细节。


-3

使用gcc intel时,关闭/实现-fno-gcse(在gfortran上运行良好)和-fno-guess-branch-prbability(在gfortran中默认)


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