GCC/Clang。如果某些 -O 标志在某台机器上是最优的,那么它们在另一台机器上也会是最优的吗?

GCC/Clang. If some -O flags are optimal in a certain machine, they will be also optimal in another machine?

我正在为我的特定代码寻找最佳优化标志。通过谷歌搜索一段时间,我发现没有选择最佳优化的黄金法则。 答案取决于具体的代码、编译器和机器

推荐的优化标志是 -O2,尽管在某些情况下 -Os(为了生成更小的二进制文件)可以生成执行速度更快的二进制文件。我宁愿忽略 -O3 和高级优化的使用,因为在某些情况下它可能很危险。在某些情况下,将 -O2-Os 标志结合使用会产生更好的结果。或者在其他情况下,使用 -march=native 的编译会为特定机器提供优化的二进制文件(因此它可以生成执行时间更短的二进制文件)。

对于我的特定代码(使用 valgrind --tool=calgrindperf stat),我发现 -march=native 不会生成最小的 运行ning 时间。

现在,我的问题是:

if for my specific code I have found that the optimal binary (I mean, binary which produces faster execution time) is generated using -Os and/or -O2, this will be optimal in other machines?.

我只想在一台计算机上确定最佳优化标志,但我必须在不同的计算机上 运行(有人使用 MacOS,其他人使用 Linux,以及他们都有 OS-版本不同)。

提前感谢您的任何建议或想法。

你其实是在指着自己的答案:

The recommended optimization flag is -O2, although in certain cases -Os

我们只需要补充一点,这种变化不仅是由于源代码(一个代码库使用 -[=21= 提供更好的结果,另一个使用 -O2 提供更好的结果),而是由于非常机器的代码是 运行ning on.

想象一下不同的处理器用于相同的指令集(无需重新编译)。一个人可以有一个小的缓存,一个 -Os 然后会产生更小的可执行文件,避免大量缓存未命中,这可能会破坏 -O2 的性能。第二个处理器作为一个巨大的缓存,所以当使用 -O2 编译代码时它不会有那么多的缓存未命中,然后允许代码在 -O2 的情况下 运行 更快。

这当然只是一个天真的简化示例,我想现实世界中的参数组合相当令人生畏。但它给了你一个提示,为什么很难提前确定最佳编译。

一些项目正在做的是:它们使用不同的目标二进制大小和指令集扩展进行编译,然后在应用程序加载时尝试确定要 运行 哪个实际二进制文件(首先在执行平台属性,做出有根据的猜测)。

TL:DR: 不,不同的 CPU 喜欢不同的东西。自动矢量化在一台机器上可能是赢家,但在另一台机器上可能是输家,如果编译器只能低效地做到这一点。

gcc -O2 -march=nativegcc -O3 -march=native 是不错的选择。或者更好的是,那些 + link-time 优化 and/or profile-guided 优化,这样编译器就知道哪些循环是热的,哪些分支通常只走一条路,哪些分支是不可预测的。

IDK 如果您曾经尝试过 gcc -march=native 而没有 -O 选项,但那将没有用; -march=native 与默认 -O0 仍然是垃圾。


-O3值得一试。 clang 手册页说它有时会使代码更大,所以要小心,并进行基准测试以确保您的代码确实更快。就性能而言,它只是 "risk",而不是正确性。如果没有其他选项专门启用不安全的优化,编译器不会改变语言规则。

Clang 文档说 -O4 目前等同于 -O3

至少对于 gcc,自动矢量化仅在 -O3 启用。 Clang 可能还有其他只发生在 -O3.

的好东西

我不确定 "general use" 的建议是否仍然只是 -O2,或者 -O3 是否足够保守,可以一直使用。使用 -fprofile-generate / -fprofile-use,编译器应该避免膨胀不常出现的 运行 路径的代码大小,并且只展开实际上很热的循环。

如果 perf 显示任何 I-cache 未命中,那么 -Os 值得尝试。也许 -Os 用于大多数源文件,而 -O3 用于具有最热门功能的源文件。

clang -O3 进行了一些循环展开,但是 gcc -O3 在没有 -fprofile-use(当然是 -funroll-loops)的情况下不会启用循环展开。

还有 -Ofast,这会启用潜在的不安全优化。如果您的代码仍然可以正常工作,那就去吧。 (我认为 unsafe 主要意味着可能会以不同方式溢出。对于 FP 代码,如果您不关心代码在存在 NaN / Inf 时的行为方式,或者不关心确切的操作顺序,那么您可以使用 -Ofast(或者只是 -O3 -ffast-math)。

如果我想花一些时间寻找最佳选项来编译我将花费大量 CPU 时间 运行 的东西,我的测试清单绝对会包括:

  • clang -O3 -march=native
  • clang -O3 -march=native -fprofile-use(在... -fprofile-generate之后)
  • 上面的 -flto -emit-llvm (link-time optimization, for whole-program optimization: inlineing functions between source files (or at least seeing if它们是否有副作用,即使没有内联。)gcc 也有 -flto
  • gcc -O3 -march=native -fno-stack-protector -fprofile-use
  • 以上 -ffast-math 甚至 -Ofast

我想 -Os 也值得一试。对齐的怪癖可以提供帮助,即使 I-cache / uop-cache 未命中不是问题。

如果 -fomit-frame-pointer 不是编译器的默认值,也可以使用它来释放额外的整数寄存器并缩短函数 prologue/epilogue.

如果您想在所有机器上使用相同的二进制文件,那么 -march=some_baseline -mtune=something。 (假设 clang 共享 gcc 的 arch/tune 选项。)

或者只是 -msse4.2 -mtune=sandybridge 之类的。我认为,只要您正在构建 x86-64 二进制文件,编译器就只会对新的 SSE 指令感兴趣。 (不是 popcnt、BMI 等)

另一种方法是在每台机器上的主目录中进行源代码检出,并使用本地编译器构建程序。但是如果你在一台机器上有一个真正新版本的 gcc 或 clang,或英特尔的编译器,那么使用它是有意义的。

您还可以查看自动并行化:gcc -ftree-parallelize-loops=n,其中 n 是要使用的线程数。


所有这一切的警告 是我听说过使用 -O3 破坏代码,因为它依赖于语言规则不需要的行为。如此激进的优化找到了一种优化方法,使代码不再做同样的事情。如果您希望您的代码 运行 快速,请确保避免未定义的行为,以便您可以将优化器一直调高。 (IIRC,最近有一个关于检查编译器何时基于假设优化某些东西的问题,因为否则会发生未定义的行为。)