Why does GCC generate 15-20% faster code if I optimize for size instead of speed?
我在2009年第一次注意到GCC(至少在我的项目和机器上)倾向于生成明显更快的代码,如果我针对大小(
我已经设法创建(相当愚蠢)代码来显示这种令人惊讶的行为,并且足够小,可以发布在这里。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | 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; } |
如果我用
(更新:我已经将所有程序集代码都移到了Github上:它们使post膨胀,并且显然对问题增加的值很小,因为
这里是使用
不幸的是,我对装配的理解非常有限,所以我不知道我接下来所做的是否正确:我抓住了
如果我猜对了,这些是用于堆栈对齐的填充。为什么GCC PAD与NOP一起工作?这样做是为了希望代码运行得更快,但显然在我的例子中,这种优化适得其反。
这起案件的罪魁祸首是填充物吗?为什么?
它产生的噪声使得时间微优化变得不可能。
当我在C或C++源代码上做微优化(与堆栈对齐无关)时,我如何确保这种偶然的幸运/不吉利对齐不干扰?
更新:
按照帕斯卡·库克的回答,我稍微调整了一下路线。通过将
-Os enables all -O2 optimizations [but] -Os disables the following optimization flags:
1
2
3 -falign-functions -falign-jumps -falign-loops <br/>
-falign-labels -freorder-blocks -freorder-blocks-and-partition <br/>
-fprefetch-loop-arrays <br/>
所以,这似乎是一个(错误的)对齐问题。
正如玛拉特杜可汗的回答所暗示的那样,我仍然对江户十一〔十五〕持怀疑态度。我不相信它不仅会干扰这个(错误)对齐问题;它对我的机器绝对没有影响。(不过,我对他的回答投了反对票。)
更新2:
我们可以把
-O2 -fno-omit-frame-pointer 0.37秒-O2 -fno-align-functions -fno-align-loops 0.37秒-S -O2 然后在work() 0.37s后手动移动add() 的总成。-O2 0.44秒
在我看来,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | 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 |
对于
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | 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 | } |
对于
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | 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 |
看来我们打电话给
我已经检查了
对于同一个可执行文件,
我把缓存未命中作为第一条评论包括在内。我检查了所有可以由
默认情况下,编译器针对"平均"处理器进行优化。由于不同的处理器支持不同的指令序列,因此由
以下是几个处理器上的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | 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 |
在某些情况下,您可以通过要求
1 2 3 4 5 | 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桥的CoreI3上,三个版本的
从
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | 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 |
从
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | 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 |
从
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | 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 |
我的同事帮我找到了一个合理的答案。他注意到256字节边界的重要性。他没有在这里注册,并鼓励我自己发布答案(并带走所有的名声)。
简短回答:
Is it the padding that is the culprit in this case? Why and how?
这一切归根结底是一致的。对齐会对性能产生重大影响,这就是为什么我们首先使用
我提交了一份(假的?)向GCC开发人员报告错误。结果显示,默认行为是"我们默认将循环与8字节对齐,但如果不需要填充超过10个字节,则尝试将其与16字节对齐。"显然,在这种特定情况下,在我的计算机上,此默认不是最佳选择。clang 3.4(trunk)与
当然,如果做了不适当的调整,事情就会变得更糟。不必要/不正确的对齐会毫无原因地占用字节,并可能增加缓存未命中等。
The noise it makes pretty much makes timing micro-optimizations
impossible.How can I make sure that such accidental lucky / unlucky alignments
are not interfering when I do micro-optimizations (unrelated to stack
alignment) on C or C++ source codes?
只需告诉GCC进行正确的校准:
长回答:
如果出现以下情况,代码将运行较慢:
一个
XX 字节边界在中间切断add() (XX 与机器有关)。如果对
add() 的调用必须跳过XX 字节边界,并且目标没有对齐。如果
add() 未对齐。如果循环没有对齐。
前2个是美丽可见的代码和结果,马拉特杜可汗亲切张贴。在这种情况下,
1 2 3 | 00000000004004fd <_ZL3addRKiS0_.isra.0>: 4004fd: 8d 04 37 lea eax,[rdi+rsi*1] 400500: c3 |
一个256字节的边界在中间向右剪切
在
1 2 3 4 5 6 7 | 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> |
没有对齐,对
如果
1 2 3 4 | 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字节的边界效果。我的机器上只有功能和循环对齐。如果我通过
I first noticed in 2009 that gcc (at least on my projects and on my
machines) have the tendency to generate noticeably faster code if I
optimize for size (-Os) instead of speed (-O2 or -O3) and I have been
wondering ever since why.
一个可能的解释是,我有对对齐敏感的热点,就像本例中的热点一样。通过干扰标志(通过
哦,还有一件事。这样的热点是如何出现的,如示例中所示的热点?像
考虑一下:
1 2 3 4 | // add.cpp int add(const int& x, const int& y) { return x + y; } |
在单独的文件中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | // 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; } |
编制为:
&GCC不会内联
仅此而已,很容易不经意地创建热点,比如操作中的热点。当然,这部分是我的错:gcc是一个优秀的编译器。如果把上面的编译为:
(内联在OP中被人为禁用,因此OP中的代码慢了2倍)。
我在补充这篇文章accept的目的是指出,已经研究了对齐对程序(包括大型程序)整体性能的影响。例如,本文(我相信这一版本也出现在CACM中)展示了链接顺序和操作系统环境大小的变化如何足以显著地改变性能。他们把这归因于"热循环"的对齐。
本文的题目是"不做任何明显错误的事情而产生错误的数据!"说由于程序运行环境中几乎不可控制的差异导致的无意的实验偏差可能会使许多基准测试结果变得毫无意义。
我认为你在同一个观察中遇到了不同的角度。
对于性能关键的代码,对于那些在安装或运行时评估环境并在不同优化版本的关键例程中选择本地最佳的系统来说,这是一个很好的理由。
我认为你可以得到与你所做的相同的结果:
I grabbed the assembly for -O2 and merged all its differences into the assembly for -Os except the .p2align lines:
…使用
另外,对于完全不同的上下文(包括不同的编译器),我注意到情况是类似的:应该"优化代码大小而不是速度"的选项针对代码大小和速度进行优化。
If I guess correctly, these are paddings for stack alignment.
不,这与堆栈无关,默认情况下生成的nop和选项-falign-*=1 prevent用于代码对齐。
According to Why does GCC pad functions with NOPs? it is done in the hope that the code will run faster but apparently this optimization backfired in my case.
Is it the padding that is the culprit in this case? Why and how?
很可能是填充物造成的。之所以认为填充是必要的并且在某些情况下是有用的,是因为代码通常是以16字节的行来获取的(有关详细信息,请参阅Agner Fog的优化资源,这些信息因处理器的型号而异)。在16字节的边界上对齐一个函数、循环或标签意味着统计上增加了包含该函数或循环所需的行数更少的可能性。显然,这会适得其反,因为这些nop会降低代码密度,从而降低缓存效率。在循环和标签的情况下,NOP甚至可能需要执行一次(当执行通常到达循环/标签时,而不是从跳转)。
如果程序受代码l1缓存的限制,那么对大小的优化就会突然开始付出代价。
当我上次检查时,编译器还不够聪明,在所有情况下都无法解决这个问题。
在您的例子中,-o3可能为两条缓存线生成足够的代码,但-os适合于一条缓存线。
我决不是这方面的专家,但我似乎记得现代处理器在分支预测方面相当敏感。用于预测分支的算法基于代码的几个属性,包括目标的距离和方向,或者至少在我编写汇编程序代码的时候。
想到的场景是小循环。当分支向后移动并且距离不太远时,分支预测正在为这种情况进行优化,因为所有的小循环都是这样做的。当您在生成的代码中交换
也就是说,我不知道如何验证,我只是想让你知道这可能是你想调查的事情。