c - gcc 的性能下降很大,可能与内联有关

标签 c performance gcc inlining

我目前正在使用 gcc 体验一些奇怪的效果(测试版本:4.8.4)。

我有一个面向性能的代码,它运行得非常快。它的速度在很大程度上取决于内联许多小函数。

由于跨多个内联 .c文件很难(-flto 尚未广泛使用),我将许多小函数(通常每个 1 到 5 行代码)保存到一个公共(public) C 文件中,我正在其中开发编解码器,及其相关解码器。按照我的标准,它“相对”大(大约 2000 行,尽管其中很多只是注释和空白行),但是将其分成更小的部分会带来新问题,因此如果可能的话,我宁愿避免这种情况。

编码器和解码器是相关的,因为它们是逆运算。但是从编程的角度来看,它们是完全分离的,除了一些 typedef 和非常低级的函数(例如从未对齐的内存位置读取)之外,没有任何共同之处。

奇怪的效果是这样的:

我最近添加了一个新功能 fnew到编码器侧。这是一个新的“切入点”。它不会在 .c 中的任何地方使用或调用文件。

它存在的简单事实使得解码器函数的性能fdec大幅下降,超过 20%,这是不容忽视的。

现在,请记住编码和解码操作是完全分离的,并且几乎没有共享,只保留一些小问题 typedef ( u32u16 等)和相关操作(读/写)。

定义新的编码函数时fnewstatic ,解码器的性能fdec增加恢复正常。自 fnew不是从 .c 调用的,我想这和它不存在一样(死代码消除)。

static fnew现在从编码器端调用,性能为fdec保持强劲。

但是一旦fnew已修改,fdec性能只是大幅下降。

假设 fnew修改越过阈值,我增加了以下gcc参数:--param max-inline-insns-auto=60 (默认情况下,它的值应该是 40。)它起作用了:fdec 的性能现在已恢复正常。

我猜这个游戏会随着 fnew 的每一个小改动而永远继续下去。或其他类似的东西,需要进一步调整。

这简直太奇怪了。函数 fnew 中的一些小修改没有合乎逻辑的原因对完全不相关的功能产生链式 react fdec ,唯一的关系是在同一个文件中。

到目前为止,我能发明的唯一尝试性解释可能是 fnew 的简单存在。足以穿越某种global file threshold这会影响 fdec . fnew可以在以下情况下设置为“不存在”:1. 不存在,2. static但不能从任何地方调用 3. static并且小到可以内联。但这只是掩盖了问题。这是否意味着我不能添加任何新功能?

真的,我在网上的任何地方都找不到任何令人满意的解释。

我很想知道是否有人已经经历了一些等效的副作用,并找到了解决方案。

[编辑]

让我们进行一些更疯狂的测试。
现在我添加了另一个完全没用的函数,只是为了玩。它的内容严格来说是 fnew 的复制粘贴,但是函数名明显不同,所以我们叫它wtf .

wtf存在,则无所谓fnew是不是静态的,max-inline-insns-auto的值是多少: fdec的表现已恢复正常。
即使 wtf不会从任何地方使用或调用... :'(

[编辑 2]
没有inline操作说明。所有功能正常或static .内联决定完全在编译器的范围内,到目前为止效果很好。

[编辑 3]
正如 Peter Cordes 所建议的,这个问题与内联无关,而是与指令对齐有关。在较新的英特尔 CPU(Sandy Bridge 和更高版本)上,热循环受益于在 32 字节边界上对齐。
问题是,默认情况下,gcc在 16 字节边界上对齐它们。根据先前代码的长度,有 50% 的机会正确对齐。因此,这是一个难以理解的问题,“看起来很随机”。

并非所有循环都是敏感的。它只对关键循环很重要,并且只有当它们的长度使它们在不太理想的对齐时跨越一个 32 字节的指令段时才重要。

最佳答案

将我的评论变成答案,因为它变成了长时间的讨论。讨论表明,性能问题对对齐很敏感。

https://stackoverflow.com/tags/x86/info 上有一些性能调整信息的链接。 ,包括 Intel 的优化指南和 Agner Fog 的非常出色的东西。 Agner Fog 的一些装配优化建议并不完全适用于 Sandybridge 和更高版本的 CPU。但是,如果您想要特定 CPU 的低级详细信息,那么微架构指南非常好。

如果没有至少一个我可以自己尝试的代码的外部链接,我只能做手波。如果您不在任何地方发布代码,您将需要使用分析/CPU 性能计数器工具,例如 Linux perf或英特尔 VTune 以在合理的时间内跟踪此问题。

在聊天中,OP 找到了 someone else having this issue, but with code posted . 这可能与 OP 看到的问题相同 ,并且是代码对齐对于 Sandybridge 风格的 uop 缓存很重要的主要方式之一。

在慢速版本的循环中间有一个 32B 边界。在边界解码为 5 uop 之前开始的指令。所以在第一个周期,uop缓存服务起来mov/add/movzbl/mov .在第二个周期中,只有一个 mov uop 留在当前缓存行中。然后第三个循环发出循环的最后 2 个 uops:addcmp+ja .

有问题的 mov开始于 0x..ff .我猜跨越 32B 边界的指令进入(其中一个)uop 缓存行作为其起始地址。

在快速版本中,一次迭代只需要 2 个周期即可发出:相同的第一个周期,然后 mov / add / cmp+ja在第二。

如果前 4 条指令中的一条多一个字节(例如用无用的前缀或 REX 前缀填充),则没有问题。在第一个缓存行的末尾不会有奇怪的人退出,因为 mov将在 32B 边界之后开始并成为下一个 uop 缓存行的一部分。

AFAIK,汇编和检查反汇编输出是使用相同指令的较长版本(参见 Agner Fog 的优化汇编)以 4 uop 的倍数获得 32B 边界的唯一方法。我不知道在您编辑时显示汇编代码对齐的 GUI。 (显然,这样做仅适用于手写 asm,而且很脆弱。根本更改代码会破坏手写对齐。)

这就是英特尔优化指南建议将关键循环与 32B 对齐的原因。

如果汇编器有办法要求使用更长的编码来汇编前面的指令以填充到特定长度,那将会非常酷。也许是 .startencodealign/.endencodealign 32指令对,将填充应用于指令之间的代码以使其在 32B 边界上结束。但是,如果使用不当,这可能会产生糟糕的代码。

对内联参数的更改将更改函数的大小,并将其他代码以 16B 的倍数覆盖。这与更改函数内容的效果类似:它变大并更改其他函数的对齐方式。

I was expecting the compiler to always make sure a function starts at ideal aligned position, using noop to fill gaps.



有一个权衡。将每个函数对齐到 64B(缓存行的开始)会损害性能。代码密度会下降,需要更多的缓存线来保存指令。 16B 很好,因为它是最新 CPU 上的指令提取/解码块大小。

Agner Fog具有每个微架构的低级细节。不过,他还没有为 Broadwell 更新它,但自 Sandybridge 以来,uop 缓存可能没有改变。我假设有一个相当小的循环主宰了运行时。我不确定首先要寻找什么。也许“慢”版本在接近 32B 代码块的末尾(因此接近 uop 高速缓存行的末尾)有一些分支目标,导致前端输出的每个时钟明显少于 4 uop。

查看“慢”和“快”版本的性能计数器(例如使用 perf stat ./cmd ),看看是否有任何不同。例如更多的缓存未命中可能表明线程之间缓存线的错误共享。另外,分析并查看“慢”版本中是否有新的热点。 (例如,在 Linux 上使用 perf record ./cmd && perf report)。

“快速”版本获得多少微指令/时钟?如果它高于 3,则对对齐敏感的前端瓶颈(可能在 uop 缓存中)可能是问题所在。如果不同的对齐方式意味着您的代码需要比可用的缓存线更多的缓存线,则该缓存或 L1/uop-cache 未命中。

无论如何,这值得重复:使用分析器/性能计数器找到新的瓶颈 “慢”版有,但“快”版没有。然后您可以花时间查看该代码块的反汇编。 (不要看 gcc 的 asm 输出。你需要在最终二进制文件的反汇编中看到对齐。)看看 16B 和 32B 的边界,因为大概它们会在两个版本之间处于不同的位置,我们认为这就是问题的原因。

如果 compare/jcc 精确分割 16B 边界,对齐也会使宏融合失败。尽管在您的情况下不太可能,因为您的函数始终与 16B 的某个倍数对齐。

回复:对齐的自动化工具:不,我不知道有什么可以查看二进制文件并告诉您任何有关对齐的有用信息。我希望有一个编辑器可以在您的代码旁边显示 4 uop 和 32B 边界的组,并在您编辑时进行更新。

Intel's IACA有时可用于分析循环,但 IIRC 不知道采取的分支,而且我认为没有复杂的前端模型,如果未对齐会破坏您的性能,这显然是问题。

关于c - gcc 的性能下降很大,可能与内联有关,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/32353161/

相关文章:

c++ - gcc:如何正确使用 __attribute((__may_alias__)) 以避免 "derefencing type-punned pointer"警告

c - 当键为字符串时如何在AVL树中查找、插入、删除节点

c - getaddrinfo() 中的段错误

c - 在 C 中获取 Linux 中已经运行的进程的标准输出

c - 使用stat获取最近修改的目录

javascript - V8 引擎如何处理添加属性的排序?

c++ - 为什么 Python 程序通常比用 C 或 C++ 编写的等效程序慢?

python - OperationTimedOut : errors={}, last_host=127.0.0.1

gcc - automake:如何设置链接描述文件的路径?

c - 即使定义了 "-nostdlib"选项,如何运行构造函数