我在2009年第一次注意到GCC(至少在我的项目和机器上)如果我优化大小(-Os)而不是速度(-O2或-O3),就倾向于生成明显更快的代码,从那时起我就一直在想为什么。
我已经设法创建了(相当愚蠢的)代码来显示这种令人惊讶的行为,并且足够小,可以在这里发布。
const int LOOP_BOUND = 200000000;
__attribute__((noinline))
static int add(const int& x, const int& y) {
return x + y;
}
__attribute__((noinline))
static int work(int xval, int yval) {
int sum(0);
for (int i=0; i<LOOP_BOUND; ++i) {
int x(xval+sum);
int y(yval+sum);
int z = add(x, y);
sum += z;
}
return sum;
}
int main(int , char* argv[]) {
int result = work(*argv[1], *argv[2]);
return result;
}
如果我用-Os编译它,执行这个程序需要0.38秒,如果用-O2或-O3编译它需要0.44秒。这些时间是一致的,几乎没有噪音(gcc 4.7.2, x86_64 GNU/Linux, Intel Core i5-3320M)。
(更新:我已经将所有汇编代码移动到GitHub:他们使帖子变得臃肿,显然对问题增加的价值很小,因为fno-align-*标志具有相同的效果。)
下面是生成的带有-Os和-O2的程序集。
不幸的是,我对汇编的理解非常有限,所以我不知道我接下来所做的是否正确:我抓取了-O2的汇编,并将其所有差异合并到-Os的汇编中,除了.p2align行,结果在这里。这段代码仍然在0.38秒内运行,唯一的区别是.p2align的东西。
如果我猜对了,这些是堆栈对齐的填充。根据为什么GCC垫功能与NOPs?这样做是希望代码运行得更快,但显然这种优化在我的例子中适得其反。
在这种情况下,填充物是罪魁祸首吗?为什么?怎么做?
它所产生的噪音几乎使计时微优化成为不可能。
当我在C或c++源代码上进行微优化(与堆栈对齐无关)时,我如何确保这种意外的幸运/不幸运的对齐不会干扰?
更新:
根据Pascal Cuoq的回答,我修补了一点对齐。通过将-O2 -fno-align-functions -fno-align-loops传递给gcc,所有的.p2align都从程序集中消失,生成的可执行文件在0.38秒内运行。根据gcc文档:
-Os启用所有-O2优化[但]-Os禁用以下优化标志:
-falign-functions -falign-跳转-falign-循环
-falign-labels -freorder-blocks -freorder-blocks-and-partition
-fprefetch-loop-arrays
所以,这似乎是一个(错误的)对齐问题。
我仍然对Marat Dukhan的回答中提出的-march=native表示怀疑。我不相信它不只是干扰这个(错误的)对齐问题;这对我的机器完全没有影响。(不过,我还是对他的回答投了赞成票。)
更新2:
我们可以把- o去掉。下面的时间是通过编译
-O2 -fno-省略帧指针0.37秒
-O2 -fno-align-functions -fno-align-loops 0.37s
-S -O2然后手动移动组装add()后工作()0.37s
- 02 0.44秒
在我看来,add()到调用站点的距离很重要。我已经尝试了性能,但性能统计和性能报告的输出对我来说意义不大。然而,我只能得到一个一致的结果:
-O2:
602,312,864 stalled-cycles-frontend # 0.00% frontend cycles idle
3,318 cache-misses
0.432703993 seconds time elapsed
[...]
81.23% a.out a.out [.] work(int, int)
18.50% a.out a.out [.] add(int const&, int const&) [clone .isra.0]
[...]
¦ __attribute__((noinline))
¦ static int add(const int& x, const int& y) {
¦ return x + y;
100.00 ¦ lea (%rdi,%rsi,1),%eax
¦ }
¦ ? retq
[...]
¦ int z = add(x, y);
1.93 ¦ ? callq add(int const&, int const&) [clone .isra.0]
¦ sum += z;
79.79 ¦ add %eax,%ebx
fno-align - *:
604,072,552 stalled-cycles-frontend # 0.00% frontend cycles idle
9,508 cache-misses
0.375681928 seconds time elapsed
[...]
82.58% a.out a.out [.] work(int, int)
16.83% a.out a.out [.] add(int const&, int const&) [clone .isra.0]
[...]
¦ __attribute__((noinline))
¦ static int add(const int& x, const int& y) {
¦ return x + y;
51.59 ¦ lea (%rdi,%rsi,1),%eax
¦ }
[...]
¦ __attribute__((noinline))
¦ static int work(int xval, int yval) {
¦ int sum(0);
¦ for (int i=0; i<LOOP_BOUND; ++i) {
¦ int x(xval+sum);
8.20 ¦ lea 0x0(%r13,%rbx,1),%edi
¦ int y(yval+sum);
¦ int z = add(x, y);
35.34 ¦ ? callq add(int const&, int const&) [clone .isra.0]
¦ sum += z;
39.48 ¦ add %eax,%ebx
¦ }
-fno-omit-frame-pointer:
404,625,639 stalled-cycles-frontend # 0.00% frontend cycles idle
10,514 cache-misses
0.375445137 seconds time elapsed
[...]
75.35% a.out a.out [.] add(int const&, int const&) [clone .isra.0] ¦
24.46% a.out a.out [.] work(int, int)
[...]
¦ __attribute__((noinline))
¦ static int add(const int& x, const int& y) {
18.67 ¦ push %rbp
¦ return x + y;
18.49 ¦ lea (%rdi,%rsi,1),%eax
¦ const int LOOP_BOUND = 200000000;
¦
¦ __attribute__((noinline))
¦ static int add(const int& x, const int& y) {
¦ mov %rsp,%rbp
¦ return x + y;
¦ }
12.71 ¦ pop %rbp
¦ ? retq
[...]
¦ int z = add(x, y);
¦ ? callq add(int const&, int const&) [clone .isra.0]
¦ sum += z;
29.83 ¦ add %eax,%ebx
在缓慢的情况下,我们似乎在对add()的调用上出现了延迟。
我已经检查了我的机器上能吐出的一切东西;不仅仅是上面给出的数据。
对于同一可执行文件,暂停周期前沿与执行时间呈线性相关;我没有注意到任何其他与之如此明显相关的东西。(比较不同可执行文件的stopped -cycles-frontend对我来说没有意义。)
我在第一个注释出现时就包含了缓存错误。我检查了所有可以在我的机器上通过perf测量的缓存失误,而不仅仅是上面给出的那些。缓存丢失是非常非常嘈杂的,并且与执行时间几乎没有相关性。
我的同事帮我找到了一个合理的答案。他注意到了256字节边界的重要性。他没有在这里注册,并鼓励我自己发布答案(并获得所有的名声)。
简短的回答:
在这种情况下,填充物是罪魁祸首吗?为什么?怎么做?
这一切都归结为结盟。对齐可以对性能产生重大影响,这就是为什么我们首先使用-falign-*标志。
我已经向gcc开发人员提交了一份(虚假的?)错误报告。事实证明,默认行为是“我们默认将循环对齐到8字节,但如果我们不需要填充超过10个字节,则尝试将其对齐到16字节。”显然,在这种特殊情况下以及在我的机器上,这种默认设置不是最佳选择。带有-O3的Clang 3.4 (trunk)进行了适当的对齐,生成的代码没有显示这种奇怪的行为。
当然,如果对齐不当,情况会更糟。一个不必要的/糟糕的对齐只会毫无理由地消耗字节,并可能增加缓存丢失等。
它产生的噪音使时间进行了微优化
不可能的。
我如何确保这种偶然的幸运/不幸的对齐
当我做微优化(与堆栈无关
对齐)的C或c++源代码?
简单地告诉gcc做正确的对齐:
g++ -O2 -falign-functions=16 -falign-loops=16
长一点的回答:
如果出现以下情况,代码运行速度会变慢:
一个XX字节的边界在中间切割add() (XX依赖于机器)。
如果对add()的调用必须跳过XX字节边界,并且目标没有对齐。
如果add()没有对齐。
如果循环没有对齐。
前两个在Marat Dukhan好心发布的代码和结果中都很明显。在这种情况下,gcc-4.8.1 -Os(在0.994秒内执行):
00000000004004fd <_ZL3addRKiS0_.isra.0>:
4004fd: 8d 04 37 lea eax,[rdi+rsi*1]
400500: c3
256字节的边界将add()切到正中间,并且add()和循环都没有对齐。惊喜,惊喜,这是最慢的情况!
在gcc-4.7.3 -Os(在0.822秒内执行)的情况下,256字节的边界只切割到冷部分(但循环和add()都不会被切割):
00000000004004fa <_ZL3addRKiS0_.isra.0>:
4004fa: 8d 04 37 lea eax,[rdi+rsi*1]
4004fd: c3 ret
[...]
40051a: e8 db ff ff ff call 4004fa <_ZL3addRKiS0_.isra.0>
没有任何东西是对齐的,对add()的调用必须跳过256字节的边界。这段代码是第二慢的。
在gcc-4.6.4 -Os(在0.709秒内执行)的情况下,虽然没有任何对齐,但对add()的调用不需要跳过256字节的边界,而目标正好在32字节之外:
4004f2: e8 db ff ff ff call 4004d2 <_ZL3addRKiS0_.isra.0>
4004f7: 01 c3 add ebx,eax
4004f9: ff cd dec ebp
4004fb: 75 ec jne 4004e9 <_ZL4workii+0x13>
这是三个中最快的。为什么256字节的边界在他的机器上是特殊的,我将把它留给他去弄清楚。我没有这样的处理器。
现在,在我的机器上,我没有这个256字节的边界效果。在我的机器上,只有函数和循环对齐生效。如果我传递g++ -O2 -falign-functions=16 -falign-loops=16,那么一切都恢复正常:我总是得到最快的情况,时间不再对-fno- ignore -frame-pointer标志敏感。我可以传递g++ -O2 -falign-functions=32 -falign-loops=32或16的任何倍数,代码也不敏感。
我在2009年第一次注意到gcc(至少在我的项目和我的
机器)倾向于生成明显更快的代码
优化的大小(-Os)而不是速度(-O2或-O3),我已经
从那以后就一直在想为什么。
一个可能的解释是,我的热点对对齐很敏感,就像这个例子中的一样。通过打乱标志(传递-Os而不是-O2),这些热点以一种幸运的方式意外对齐,代码变得更快。这与优化大小无关:这些完全是偶然的,热点对齐得更好。从现在开始,我将检查对齐对我的项目的影响。
哦,还有一件事。这样的热点是如何出现的,就像例子中显示的那样?像add()这样的小函数的内联怎么会失败呢?
考虑一下:
// add.cpp
int add(const int& x, const int& y) {
return x + y;
}
在一个单独的文件中:
// main.cpp
int add(const int& x, const int& y);
const int LOOP_BOUND = 200000000;
__attribute__((noinline))
static int work(int xval, int yval) {
int sum(0);
for (int i=0; i<LOOP_BOUND; ++i) {
int x(xval+sum);
int y(yval+sum);
int z = add(x, y);
sum += z;
}
return sum;
}
int main(int , char* argv[]) {
int result = work(*argv[1], *argv[2]);
return result;
}
编译为:g++ -O2 add.cpp main.cpp。
gcc不会内联add()!
就这样,很容易无意中创建像op中那样的热点。当然,这在一定程度上是我的错:gcc是一个优秀的编译器。如果将上面的代码编译为:g++ -O2 -flto add.cpp main.cpp,也就是说,如果我进行链接时间优化,代码在0.19秒内运行!
(内联在OP中被人为禁用,因此,OP中的代码慢了2倍)。
默认情况下,编译器针对“普通”处理器进行优化。由于不同的处理器支持不同的指令序列,通过-O2启用的编译器优化可能会使一般处理器受益,但会降低特定处理器的性能(同样适用于-Os)。如果您在不同的处理器上尝试相同的示例,您会发现其中一些处理器受益于-O2,而其他处理器更有利于-Os优化。
下面是几个处理器上time ./test 0 0的结果(报告用户时间):
Processor (System-on-Chip) Compiler Time (-O2) Time (-Os) Fastest
AMD Opteron 8350 gcc-4.8.1 0.704s 0.896s -O2
AMD FX-6300 gcc-4.8.1 0.392s 0.340s -Os
AMD E2-1800 gcc-4.7.2 0.740s 0.832s -O2
Intel Xeon E5405 gcc-4.8.1 0.603s 0.804s -O2
Intel Xeon E5-2603 gcc-4.4.7 1.121s 1.122s -
Intel Core i3-3217U gcc-4.6.4 0.709s 0.709s -
Intel Core i3-3217U gcc-4.7.3 0.708s 0.822s -O2
Intel Core i3-3217U gcc-4.8.1 0.708s 0.944s -O2
Intel Core i7-4770K gcc-4.8.1 0.296s 0.288s -Os
Intel Atom 330 gcc-4.8.1 2.003s 2.007s -O2
ARM 1176JZF-S (Broadcom BCM2835) gcc-4.6.3 3.470s 3.480s -O2
ARM Cortex-A8 (TI OMAP DM3730) gcc-4.6.3 2.727s 2.727s -
ARM Cortex-A9 (TI OMAP 4460) gcc-4.6.3 1.648s 1.648s -
ARM Cortex-A9 (Samsung Exynos 4412) gcc-4.6.3 1.250s 1.250s -
ARM Cortex-A15 (Samsung Exynos 5250) gcc-4.7.2 0.700s 0.700s -
Qualcomm Snapdragon APQ8060A gcc-4.8 1.53s 1.52s -Os
在某些情况下,你可以通过让gcc为你的特定处理器进行优化来缓解不利优化的影响(使用选项-mtune=native或-march=native):
Processor Compiler Time (-O2 -mtune=native) Time (-Os -mtune=native)
AMD FX-6300 gcc-4.8.1 0.340s 0.340s
AMD E2-1800 gcc-4.7.2 0.740s 0.832s
Intel Xeon E5405 gcc-4.8.1 0.603s 0.803s
Intel Core i7-4770K gcc-4.8.1 0.296s 0.288s
更新:在基于Ivy bridge的Core i3上,gcc的三个版本(4.6.4、4.7.3和4.8.1)生成的二进制文件的性能有显著不同,但汇编代码只有细微的变化。到目前为止,我还没有对这个事实做出解释。
gcc-4.6.4 -Os的汇编(在0.709秒内执行):
00000000004004d2 <_ZL3addRKiS0_.isra.0>:
4004d2: 8d 04 37 lea eax,[rdi+rsi*1]
4004d5: c3 ret
00000000004004d6 <_ZL4workii>:
4004d6: 41 55 push r13
4004d8: 41 89 fd mov r13d,edi
4004db: 41 54 push r12
4004dd: 41 89 f4 mov r12d,esi
4004e0: 55 push rbp
4004e1: bd 00 c2 eb 0b mov ebp,0xbebc200
4004e6: 53 push rbx
4004e7: 31 db xor ebx,ebx
4004e9: 41 8d 34 1c lea esi,[r12+rbx*1]
4004ed: 41 8d 7c 1d 00 lea edi,[r13+rbx*1+0x0]
4004f2: e8 db ff ff ff call 4004d2 <_ZL3addRKiS0_.isra.0>
4004f7: 01 c3 add ebx,eax
4004f9: ff cd dec ebp
4004fb: 75 ec jne 4004e9 <_ZL4workii+0x13>
4004fd: 89 d8 mov eax,ebx
4004ff: 5b pop rbx
400500: 5d pop rbp
400501: 41 5c pop r12
400503: 41 5d pop r13
400505: c3 ret
gcc-4.7.3 -Os的程序集(在0.822秒内执行):
00000000004004fa <_ZL3addRKiS0_.isra.0>:
4004fa: 8d 04 37 lea eax,[rdi+rsi*1]
4004fd: c3 ret
00000000004004fe <_ZL4workii>:
4004fe: 41 55 push r13
400500: 41 89 f5 mov r13d,esi
400503: 41 54 push r12
400505: 41 89 fc mov r12d,edi
400508: 55 push rbp
400509: bd 00 c2 eb 0b mov ebp,0xbebc200
40050e: 53 push rbx
40050f: 31 db xor ebx,ebx
400511: 41 8d 74 1d 00 lea esi,[r13+rbx*1+0x0]
400516: 41 8d 3c 1c lea edi,[r12+rbx*1]
40051a: e8 db ff ff ff call 4004fa <_ZL3addRKiS0_.isra.0>
40051f: 01 c3 add ebx,eax
400521: ff cd dec ebp
400523: 75 ec jne 400511 <_ZL4workii+0x13>
400525: 89 d8 mov eax,ebx
400527: 5b pop rbx
400528: 5d pop rbp
400529: 41 5c pop r12
40052b: 41 5d pop r13
40052d: c3 ret
gcc-4.8.1 -Os中的程序集(在0.994秒内执行):
00000000004004fd <_ZL3addRKiS0_.isra.0>:
4004fd: 8d 04 37 lea eax,[rdi+rsi*1]
400500: c3 ret
0000000000400501 <_ZL4workii>:
400501: 41 55 push r13
400503: 41 89 f5 mov r13d,esi
400506: 41 54 push r12
400508: 41 89 fc mov r12d,edi
40050b: 55 push rbp
40050c: bd 00 c2 eb 0b mov ebp,0xbebc200
400511: 53 push rbx
400512: 31 db xor ebx,ebx
400514: 41 8d 74 1d 00 lea esi,[r13+rbx*1+0x0]
400519: 41 8d 3c 1c lea edi,[r12+rbx*1]
40051d: e8 db ff ff ff call 4004fd <_ZL3addRKiS0_.isra.0>
400522: 01 c3 add ebx,eax
400524: ff cd dec ebp
400526: 75 ec jne 400514 <_ZL4workii+0x13>
400528: 89 d8 mov eax,ebx
40052a: 5b pop rbx
40052b: 5d pop rbp
40052c: 41 5c pop r12
40052e: 41 5d pop r13
400530: c3 ret