我目前正在使用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.)并且它有效:{的性能{1}}现已恢复正常。
我猜这个游戏会在fdec
或其他任何类似的修改后永远持续下去,需要进一步调整。
这简直太奇怪了。函数fnew
中的一些小修改没有逻辑上的原因可以对完全不相关的函数fnew
产生连锁效应,只有关系才能在同一个文件中。
到目前为止,我可以发明的唯一一个试探性的解释是,fdec
的简单存在可能足以跨越某种fnew
,这会影响global file threshold
。 fdec
可以在以下情况下“不存在”:1。不在那里,2。fnew
但不从任何地方调用3. static
并且小到可以内联。但它只是隐藏了这个问题。这是否意味着我无法添加任何新功能?
真的,我在网上找不到任何令人满意的解释。
我很想知道某人是否已经经历过一些等效的副作用,并找到了解决方法。
[编辑]
让我们去做一些更疯狂的测试。
现在我正在添加另一个完全无用的功能,只是为了玩。它的内容完全是static
的复制粘贴,但函数的名称明显不同,所以我们称之为fnew
。
当wtf
存在时,wtf
是否为静态无关紧要,fnew
的值是什么无关紧要:max-inline-insns-auto
的效果恢复正常。
即使fdec
没有使用也没有从任何地方调用......:'(
[编辑2]
没有wtf
指令。所有功能都是正常的或inline
。内联决策完全在编译器的范围内,到目前为止工作正常。
[编辑3]
正如Peter Cordes所建议的那样,这个问题与内联无关,而与指令对齐无关。在较新的Intel cpus(Sandy Bridge及更高版本)上,热循环可以在32字节边界上进行对齐。
问题是,默认情况下,static
将它们对齐在16字节边界上。根据前面代码的长度,它有50%的机会进行正确对齐。因此,一个难以理解的问题,“看起来随机”。
并非所有循环都是敏感的。它只适用于关键循环,并且只有当它们的长度使它们在理想情况下不那么对齐时才会跨越另一个32字节的指令段。
答案 0 :(得分:2)
将我的评论转化为答案,因为它正在变成一个长时间的讨论。讨论表明,性能问题对齐很敏感。
在https://stackoverflow.com/tags/x86/info处有一些指向性能调整信息的链接,包括英特尔的优化指南和Agner Fog非常出色的内容。 Agner Fog的一些装配优化建议并不完全适用于Sandybridge和后来的CPU。但是,如果你想要特定CPU的低级细节,那么microarch指南非常好。
如果没有至少一个代码的外部链接,我可以尝试自己,我不能做手动。如果您不在任何时间发布代码,那么您将需要使用分析/ CPU性能计数器工具(如Linux perf
或Intel VTune)在合理的时间内对其进行跟踪。 / p>
在聊天中,OP找到someone else having this issue, but with code posted。 这可能与OP看到的问题相同,并且是代码对齐对Sandybridge风格的uop缓存很重要的主要方式之一。
在慢速版本的循环中间有32B边界。在边界解码之前开始的指令为5微秒。因此,在第一个周期中,uop缓存提供mov/add/movzbl/mov
。在第二个周期中,当前缓存行中只剩下一个mov
uop。然后第3个循环周期发出循环的最后2个uops:add
和cmp+ja
。
有问题的mov
从0x..ff
开始。我想跨越32B边界的指令进入(其中一个)uop cacheline(s)作为起始地址。
在快速版本中,迭代只需要2个周期来发布:相同的第一个周期,然后是第二个周期中的mov / add / cmp+ja
。
如果前4个指令中的一个长一个字节(例如用无用前缀或REX前缀填充),则没有问题。在第一个高速缓存行的末尾不会出现奇数输出,因为mov
将在32B边界之后开始并成为下一个高速缓存行的一部分。
AFAIK,汇编&检查反汇编输出是使用更长版本的相同指令(参见Agner Fog'优化装配)的唯一方法,以获得4个uop的倍数的32B边界。我不知道GUI会在您编辑时显示已组合代码的对齐方式。 (显然,这样做只适用于手写的asm,而且很脆弱。改变代码会破坏手动对齐。)
这就是为什么英特尔的优化指南建议将关键循环对齐到32B。
如果汇编程序有办法要求使用较长的编码组装前面的指令以填充到一定长度,那将是非常酷的。也许是一对.startencodealign
/ .endencodealign 32
指令,用于在指令之间的代码中应用填充以使其在32B边界上结束。如果使用得不好,这可能会造成可怕的代码。
对内联参数的更改将更改函数的大小,并将其他代码更改为多个16B。这与更改函数内容的效果类似:它变大并改变其他函数的对齐方式。
我希望编译器始终确保函数从...开始 理想的对齐位置,使用noop填补空白。
有一个权衡。将每个函数与64B(高速缓存行的开始)对齐会损害性能。代码密度会下降,需要更多缓存行来保存指令。 16B很好,因为它是最近CPU上的指令获取/解码块大小。
Agner Fog包含每个微阵列的低级详细信息。尽管如此,他还没有为Broadwell更新它,但是自Sandybridge以来,uop缓存可能没有改变。我假设有一个相当小的循环支配运行时。我不确定要先找什么。也许"慢" version在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的某个倍数对齐。
re:用于对齐的自动化工具:不,我不知道任何可以查看二进制文件并告诉您有关对齐的任何有用信息。我希望有一个编辑器在您的代码旁边显示4个uops和32B边界组,并在编辑时更新。
Intel's IACA有时可用于分析循环,但是IIRC它不知道所采用的分支,我认为没有一个复杂的前端模型,这显然是如果错位会破坏您的表现,则会出现问题。
答案 1 :(得分:0)
根据我的经验,性能下降可能是由于禁用内联优化造成的。
' inline'修饰符并不表示强制函数内联。它为编译器提供内联函数的提示。因此,当编译器的内联优化标准不能通过对代码的微不足道的修改来满足时,使用内联修改的函数通常被编译为静态函数。
并且有一个问题是使问题更复杂,嵌套内联优化。如果你有一个内联函数fA,它调用内联函数fB,如下所示:
inline void fB(int x, int y) {
return x * y;
}
inline void fA() {
for(int i = 0; i < 0x10000000; ++i) {
fB(i, i+1);
}
}
void main() {
fA();
}
在这种情况下,我们希望fA和fB都是内联的。但如果不符合内联criteia,性能就无法预测。也就是说,当内联对fB禁用时会出现大的性能下降,但fA会略微下降。而且你知道,编译器的内部决策非常复杂。
导致禁用内联的原因,例如,内联函数的大小,.c文件的大小,局部变量的数量等。
实际上,在C#中,我经历过这种性能下降。在我的例子中,当一个局部变量被添加到一个简单的内联函数时,会出现60%的性能下降。
您可以通过阅读已编译的汇编代码来调查会发生什么。我想对使用&#39; inline&#39;。
修改的函数有意想不到的真实调用