为什么gcc生成15-20%更快的代码,如果我优化大小,而不是速度?

我首先注意到2009年gcc(至less在我的项目和我的机器上)有倾向于生成明显更快的代码,如果我优化大小-Os ),而不是速度( -O2-O3 ),我一直想知道为什么。

我已经设法创build(相当愚蠢的)代码,显示这个令人惊讶的行为,并足够小,在这里张贴。

 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 ,执行该程序需要-Os ,如果使用-O2-O3 -Os ,则需要-O3 。 这些时间是一致的,几乎没有噪音(gcc 4.7.2,x86_64 GNU / Linux,Intel Core i5-3320M)。

(更新:我已经将所有汇编代码移动到GitHub :他们使这个post变得臃肿,显然增加了很less的价值,因为fno-align-*标志具有相同的效果。)

生成的组件与-Os-O2 。 不幸的是,我对程序集的理解是非常有限的,所以我不知道我下一步做的是否正确:我抓住了-O2的程序集,并将其所有的差异合并到除了 .p2align之外.p2align的程序.p2align ,结果在这里 。 此代码仍然运行在0.38s, 唯一的区别是 .p2align 东西。

如果我猜对了,这些是堆栈alignment的填充。 根据为什么GCC垫与NOPfunction? 它是在希望代码将运行得更快,但显然这种优化在我的情况下反弹。

在这种情况下,填补是罪魁祸首吗? 为什么和如何?

它所造成的噪音几乎使微时间的优化成为可能。

当我在C或C ++源代码上进行微优化(与堆栈alignment无关)时,如何确保这种意外的幸运/不吉利的alignment不会干扰?


更新:

在Pascal Cuoq的回答之后,我对比赛进行了一些调整。 通过传递-O2 -fno-align-functions -fno-align-loops到gcc,所有的.p2align都从程序.p2align消失了,生成的可执行文件运行在0.38s。 根据gcc文档 :

-Os启用所有-O2优化[但是] -Os禁用以下优化标志:

  -falign-functions -falign-jumps -falign-loops <br/> -falign-labels -freorder-blocks -freorder-blocks-and-partition <br/> -fprefetch-loop-arrays <br/> 

所以,这似乎是一个(错)alignment问题。

我对马拉特·杜汉的回答中所提出的“ -march=native仍持怀疑态度。 我不相信这不仅仅是干扰这个(错误的)alignment问题, 它对我的机器绝对没有影响。 (不过,我提高了他的答案。)


更新2:

我们可以从图片中删除。 下面的时间是通过编译得到的

  • -O2 -fno-omit-frame-pointer 0.37s

  • -O2 -fno-align-functions -fno-align-loops 0.37s

  • -S -O2然后在work() 0.37s之后手动移动add()的程序集

  • -O2 0.44s

看起来来自呼叫站点的add()距离很重要。 我已经尝试过perf ,但perf statperf report的输出对我来说没有什么意义。 但是,我只能得到一个一致的结果:

-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()

我已经检查了perf -e可以在我的机器上吐出来的所有东西 ; 不只是上面给出的统计数据。

对于同一个可执行文件, stalled-cycles-frontend与执行时间呈线性关系; 我没有注意到其他任何会如此清楚相关的东西。 (比较不同可执行文件的stalled-cycles-frontend对我来说没有意义。)

我把第一条评论写进了caching未命中。 我检查了可以在我的机器上测量的所有caching未命中,而不仅仅是上面给出的。 caching缺失非常非常嘈杂,与执行时间几乎没有关系。

默认情况下,编译器对“平均”处理器进行优化。 由于不同的处理器有利于不同的指令序列,因此-O2启用的编译器优化可能会使平均处理器受益,但会降低您的特定处理器的性能(对于-Os同样如此)。 如果您在不同的处理器上尝试相同的示例,则会发现其中一些可从-O2获益,而其他则对-Os优化更有利。

下面是time ./test 0 0的结果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上, gcc4.6.44.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 

我的同事帮我find了一个合理的答案。 他注意到了256字节边界的重要性。 他没有在这里注册,并鼓励我自己发表答案(并取得所有的名气)。


简短的回答:

在这种情况下,填补是罪魁祸首吗? 为什么和如何?

这一切都归结为alignment。 alignment可以对性能产生重大影响,这就是为什么我们首先有-falign-*标志。

我已经向gcc开发者提交了一个(伪造的)错误报告 。 事实certificate,默认行为是“我们将循环默认alignment8个字节,但如果我们不需要填写10个字节的话,尝试将它alignment到16个字节”。 显然,这个默认不是在这种特殊情况下和我的机器上的最佳select。 与-O3叮当3.4(树干)做适当的alignment和生成的代码不显示这种奇怪的行为。

当然, 如果不恰当的alignment,就会让事情变得更糟。 一个不必要的/错误的alignment只是无意中吃掉了字节,并可能增加caching失误等等。

它所造成的噪音几乎使微时间的优化成为可能。

当我在C或C ++源代码上进行微型优化(与堆栈alignment无关)时,如何确保这种偶然的幸运/不吉利的alignment不会干扰?

只要告诉gcc做正确的alignment:

g++ -O2 -falign-functions=16 -falign-loops=16


很长的回答:

代码将运行较慢,如果:

  • 一个XX字节边界在中间切割add()XX依赖于机器)。

  • 如果调用add()必须跳过一个XX字节的边界,并且目标没有alignment。

  • 如果add()不alignment。

  • 如果循环未alignment。

前两名在杜拉夫(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()和循环都不alignment。 惊喜,惊喜,这是最慢的情况!

如果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> 

没有任何alignment,并且对add()的调用必须跳过256字节边界。 这个代码是第二个最慢的。

gcc-4.6.4 -Os (执行时间为0.709秒)的情况下,虽然没有任何alignment,但对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字节的边界效应。 只有function和循环alignment在我的机器上踢。 如果我通过g++ -O2 -falign-functions=16 -falign-loops=16那么一切都恢复正常了:我总是得到最快的情况,时间对-fno-omit-frame-pointer标志不再敏感。 我可以通过g++ -O2 -falign-functions=32 -falign-loops=32或16的任何倍数,代码对此也不敏感。

我在2009年首先注意到,如果我优化大小(-Os)而不是速度(-O2或-O3),gcc(至less在我的项目和我的机器上)有生成明显更快的代码的倾向,我一直在想至今为止。

一个可能的解释是,我有像这个例子中那样对alignment敏感的热点。 通过弄乱旗子(传递-Os而不是-O2 ),这些热点偶然以一种幸运的方式被排列,并且代码变得更快。 这与优化尺寸没有任何关系:这些都是纯粹的意外,热点更好地alignment。 从现在开始,我将检查alignment对我的项目的影响。

哦,还有一件事。 这样的热点怎么会出现,就像例子中显示的那样? 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()

就是这样,很容易意外地创build像OP中那样的热点。 当然,这部分是我的错:gcc是一个优秀的编译器。 如果将上面的g++ -O2 -flto add.cpp main.cpp编译为: g++ -O2 -flto add.cpp main.cpp ,也就是说, 如果我执行链接时间优化,代码运行在0.19s!

(内联在OP中被人为禁用,因此OP中的代码慢了2倍)。

我join这个接受后指出,调整对整体performance的影响,包括大的程序已经被研究。 例如, 本文 (我相信这个版本也出现在CACM中)显示了链接顺序和操作系统环境大小如何变化足以显着改变性能。 他们将这归因于“热循环”的alignment。

这篇文章题为“产生错误的数据而不做任何明显的错误!” 说由于在程序运行环境中几乎不可控制的差异而导致的无意的实验偏差可能使许多基准结果变得毫无意义。

我认为你在同一个观察中遇到了不同的angular度。

对于性能关键的代码,对于在安装或运行时评估环境的系统来说,这是一个非常好的参数,并在不同优化版本的关键例程中select本地最佳。

我认为你可以得到和你所做的一样的结果:

我抓住了-O2的程序集,并将其所有的差异合并到除了.p2align行之外的-Os集合中:

…使用-O2 -falign-functions=1 -falign-jumps=1 -falign-loops=1 -falign-labels=1 。 我一直在用这些选项来编译所有的东西,这些东西比我想象的要快15年。

此外,对于完全不同的上下文(包括不同的编译器),我注意到情况是相似的 :应该“优化代码大小而不是速度”的选项优化代码大小和速度。

如果我猜对了,这些是堆栈alignment的填充。

不,这与堆栈无关,默认生成的NOP和选项-falign – * = 1是为了代码alignment。

根据为什么GCC垫与NOPfunction? 这是希望代码将运行得更快,但显然这种优化在我的情况下倒退。

在这种情况下,填补是罪魁祸首吗? 为什么和如何?

填充是很有可能的罪魁祸首。 认为填充是必要的,在某些情况下是有用的代码通常是在16个字节的行(参阅Agner Fog的优化资源的细节,这取决于处理器的型号)。 在一个16字节的边界上alignment一个函数,循环或者标签意味着统计数据的可能性增加了,那么包含这个函数或者循环所需的行就会less一个。 显然,这种NOP降低了代码密度,因此缓解了效率。 在循环和标签的情况下,NOP甚至可能需要执行一次(当执行到达循环/标签正常,而不是跳转)。

如果你的程序受到CODE L1caching的限制,那么对大小的优化突然开始付出。

当最后一次检查时,编译器不够聪明,无法在所有情况下找出这个问题。

在你的情况下,-O3可能为两个caching行生成足够的代码,但-Os适合一个caching行。

我绝不是这方面的专家,但我似乎记得现代处理器在分支预测方面非常敏感。 用于预测分支的algorithm(或者至less在我写汇编代码的时候已经回来了)基于代码的几个属性,包括目标的距离和方向。

想到的情况是小循环。 当分支倒退并且距离不太远时,分支预测正在对这种情况进行优化,因为所有的小循环都是这样完成的。 当您在生成的代码中交换addwork的位置或者两者的位置略有变化时,可能会发挥相同的规则。

这就是说,我不知道如何validation,我只是想让你知道,这可能是你想看的东西。