Cya*_*yan 10 c performance gcc inlining
我目前正在经历一些奇怪的效果gcc
(测试版本:4.8.4).
我有一个性能导向的代码,运行速度非常快.它的速度在很大程度上取决于内联许多小功能.
由于内联多个.c
文件很困难(-flto
尚未广泛使用),因此我将许多小函数(通常每行1到5行)保存到一个公共C文件中,我正在开发一个编解码器,其相关解码器.根据我的标准,它"相对"大(约2000行,虽然其中很多只是注释和空白行),但将它分成更小的部分会带来新的问题,所以如果可能的话,我宁愿避免这样做.
编码器和解码器是相关的,因为它们是逆运算.但是从编程的角度来看,它们是完全分离的,除了少量的typedef和非常低级的函数(例如从未对齐的内存位置读取)之外,没有任何共同点.
奇怪的效果是这个:
我最近fnew
在编码器方面添加了一个新功能.这是一个新的"切入点".它不会在.c
文件中的任何位置使用或调用.
它存在的简单事实使得解码器功能的性能fdec
大幅下降超过20%,这是太多不容忽视的.
现在,记住比编码和解码操作是完全分开的,并分享几乎没有,节省一些小的typedef
(u32
,u16
关系等)以及相关的操作(读/写).
当定义新的编码功能fnew
如static
,解码器的性能fdec
提高了恢复正常.既然fnew
没有从中调用.c
,我想它就像它不存在一样(死代码消除).
如果static fnew
现在从编码器端调用,则性能fdec
仍然很强.
但是一旦fnew
修改,fdec
性能就会大幅下降.
假设fnew
修改超过了一个阈值,我增加了以下gcc
参数:( --param max-inline-insns-auto=60
默认情况下,它的值应该是40.)并且它起作用:性能fdec
现在恢复正常.
而且我猜这个游戏将永远持续每一个小修改fnew
或其他类似的东西,需要进一步调整.
这简直太奇怪了.fnew
对于完全不相关的函数具有连锁效应,函数中的一些小修改是没有逻辑上的原因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所建议的那样,这个问题与内联无关,而与指令对齐无关.在较新的Intel cpus(Sandy Bridge及更高版本)上,热循环可以在32字节边界上进行对齐.问题是,默认情况下,gcc
它们在16字节边界上对齐.根据前面代码的长度,这有50%的机会进行正确对齐.因此,一个难以理解的问题,"看起来随机".
并非所有循环都是敏感的.它只对关键循环很重要,并且只有当它们的长度使它们在理想情况下对齐时才会跨越另一个32字节的指令段.
将我的评论变成答案,因为它正在变成一场漫长的讨论。讨论表明性能问题对对齐很敏感。
https://stackoverflow.com/tags/x86/info上有一些性能调整信息的链接,包括英特尔的优化指南和 Agner Fog 的非常优秀的东西。Agner Fog 的一些汇编优化建议并不完全适用于 Sandybridge 和更高版本的 CPU。不过,如果您想要特定 CPU 的底层详细信息,微架构指南非常好。
如果没有至少一个我可以自己尝试的代码的外部链接,我只能用手挥手。如果您不将代码发布到任何地方,您将需要使用 Linuxperf
或 Intel VTune 等分析/CPU 性能计数器工具在合理的时间内进行跟踪。
在聊天中,OP发现其他人也有这个问题,但发布了代码。 这可能与 OP 遇到的问题相同,并且是代码对齐对于 Sandybridge 风格的 uop 缓存至关重要的主要方式之一。
在慢速版本中,循环中间有一个 32B 边界。在边界之前开始的指令解码为 5 uop。因此,在第一个周期中,uop 缓存将提供服务mov/add/movzbl/mov
。mov
在第二个周期中,当前缓存行中只剩下一个微指令。然后第 3 个周期发出循环的最后 2 个微指令:add
和cmp+ja
。
问题mov
始于0x..ff
. 我猜想跨越 32B 边界的指令会进入(其中一个)uop 高速缓存行作为其起始地址。
在快速版本中,迭代只需要 2 个周期即可发出:相同的第一个周期,然后mov / add / cmp+ja
是第二个周期。
如果前 4 条指令中的一条指令长了一个字节(例如用无用的前缀或 REX 前缀填充),则不会有问题。第一个高速缓存行的末尾不会出现奇怪的情况,因为它将mov
在 32B 边界之后开始,并成为下一个 uop 高速缓存行的一部分。
AFAIK,汇编和检查反汇编输出是使用相同指令的较长版本(请参阅 Agner Fog 的优化汇编)以 4 微指令的倍数获得 32B 边界的唯一方法。我不知道有哪个 GUI 可以在您编辑时显示汇编代码的对齐情况。(显然,这样做仅适用于手写汇编,并且很脆弱。更改代码会破坏手写对齐。)
这就是为什么 Intel 的优化指南建议将关键循环调整为 32B。
如果汇编器有一种方法可以请求使用更长的编码来组装前面的指令以填充到一定的长度,那真是太酷了。可能是一对.startencodealign
/.endencodealign 32
指令,对指令之间的代码应用填充,使其在 32B 边界上结束。不过,如果使用不当,这可能会产生糟糕的代码。
对内联参数的更改将更改函数的大小,并使其他代码超出 16B 的倍数。这与更改函数内容的效果类似:它变大并改变其他函数的对齐方式。
我期望编译器始终确保函数从理想的对齐位置开始,使用 noop 来填补空白。
这是一个权衡。将每个函数与 64B(缓存行的开头)对齐会损害性能。代码密度将会下降,需要更多的缓存行来保存指令。16B 很好,因为它是最新 CPU 上的指令获取/解码块大小。
Agner Fog拥有每个微体系结构的底层详细信息。不过,他还没有为 Broadwell 更新它,但自 Sandybridge 以来,uop 缓存可能没有改变。我假设有一个相当小的循环主宰运行时。我不确定首先要寻找什么。也许“慢”版本在 32B 代码块末尾附近有一些分支目标(因此在 uop 缓存行末尾附近),导致从前端发出的每个时钟明显少于 4 个 uop。
查看“慢”和“快”版本的性能计数器(例如使用perf stat ./cmd
),看看是否有不同。例如,更多的缓存未命中可能表明线程之间缓存行的错误共享。另外,分析并查看“慢”版本中是否有新的热点。(例如perf record ./cmd && perf report
在 Linux 上)。
“快速”版本获得多少微指令/时钟?如果它高于 3,则对对齐敏感的前端瓶颈(可能在 uop 缓存中)可能是问题所在。如果不同的对齐意味着您的代码需要的缓存行多于可用的缓存行,则 L1 / uop-cache 会丢失。
无论如何,这值得重复:使用分析器/性能计数器来查找“慢”版本具有但“快”版本没有的新瓶颈。然后您可以花时间查看该代码块的反汇编。(不要看 gcc 的 asm 输出。您需要查看最终二进制文件的反汇编中的对齐情况。)查看 16B 和 32B 边界,因为它们可能位于两个版本之间的不同位置,我们认为这就是问题的原因。
如果比较/jcc 精确地分割了 16B 边界,则对齐也会使宏融合失败。尽管这在您的情况下不太可能,因为您的函数始终与 16B 的某个倍数对齐。
回复:用于对齐的自动化工具:不,我不知道有什么可以查看二进制文件并告诉您有关对齐的任何有用信息。我希望有一个编辑器可以在代码旁边显示 4 uop 和 32B 边界组,并在编辑时进行更新。
英特尔的 IACA有时可用于分析循环,但 IIRC 它不知道已采取的分支,而且我认为没有复杂的前端模型,如果未对齐会破坏性能,这显然是问题。