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

13 浏览
0 Comments

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

2009年,我首次注意到GCC(至少在我的项目和我的计算机上)在优化尺寸(-Os)而不是速度(-O2或-O3)时,有生成明显更快代码的倾向,并且一直以来我一直在思考为什么会这样。我已经成功创建了一个(相当愚蠢的)代码,展示了这种令人惊讶的行为,并且足够小可以在这里发布。\n如果我使用-Os编译它,执行此程序需要0.38秒,如果使用-O2或-O3编译,则需要0.44秒。这些时间是一致的,几乎没有噪音(gcc 4.7.2,x86_64 GNU/Linux,Intel Core i5-3320M)。\n这里是使用-Os和-O2生成的汇编代码。\n不幸的是,我对汇编的理解非常有限,所以我不知道接下来我做的事情是否正确:我获取了-O2的汇编代码,并将其所有差异合并到-Os的汇编代码中,除了.p2align行外,结果在这里。这段代码仍然在0.38秒内运行,并且唯一的区别是.p2align部分。\n如果我猜得正确,这些是用于栈对齐的填充。根据“为什么GCC使用NOP填充函数?”中的解释,这是为了提高代码的运行速度,但显然这种优化在我的情况下适得其反。\n在进行微观优化(与栈对齐无关)时,它产生的噪音使得计时微观优化几乎不可能。\n我该如何确保在对C或C++源代码进行微观优化时,这种意外的幸运/不幸的对齐不会干扰?\n更新:\n根据Pascal Cuoq的回答,我对对齐进行了一些调整。通过向gcc传递-O2 -fno-align-functions -fno-align-loops,汇编中的所有.p2align都消失了,并且生成的可执行文件在0.38秒内运行。根据gcc文档:\n“-Os启用所有-O2优化[但]-Os禁用以下优化标志:-falign-functions -falign-jumps -falign-loops -falign-labels -freorder-blocks -freorder-blocks-and-partition -fprefetch-loop-arrays”\n因此,这似乎基本上是一个(错误的)对齐问题。\n我对Marat Dukhan的建议中的-march=native持怀疑态度。我不相信它不会干扰这个(错误的)对齐问题;它在我的机器上没有任何影响。(尽管如此,我对他的回答进行了点赞。)\n更新2:\n我们可以将-Os从中排除。通过使用以下编译选项进行编译,得到以下时间:\n-O2 -fno-omit-frame-pointer 0.37秒\n-O2 -fno-align-functions -fno-align-loops 0.37秒\n-S -O2然后手动将add()的汇编代码移动到work()之后 0.37秒\n-O2 0.44秒\n对我来说,add()与调用点之间的距离非常重要。我尝试了perf,但perf stat和perf report的输出对我来说意义不大。但是,我只能得到一个一致的结果:\n-O2:\n602,312,864个前端停顿周期,前端周期空闲率为0.00%\n3,318个缓存失效\n运行时间为0.432703993秒\n[...]\na.out中81.23%的时间在work(int, int)函数中\na.out中18.50%的时间在add(int const&, int const&) [clone .isra.0]函数中\n[...]\nint add(const int& x, const int& y)函数中:\nlea (%rdi,%rsi,1),%eax\n? retq\n[...]\nint z = add(x, y);\n? callq add(int const&, int const&) [clone .isra.0]\nsum += z;\nadd %eax,%ebx\n对于-fno-align-*:\n604,072,552个前端停顿周期,前端周期空闲率为0.00%\n9,508个缓存失效\n运行时间为0.375681928秒\n[...]\na.out中82.58%的时间在work(int, int)函数中\na.out中16.83%的时间在add(int const&, int const&) [clone .isra.0]函数中\n[...]\nint add(const int& x, const int& y)函数中:\nlea (%rdi,%rsi,1),%eax\nwork(int xval, int yval)函数中:\nlea 0x0(%r13,%rbx,1),%edi\nint z = add(x, y);\n? callq add(int const&, int const&) [clone .isra.0]\nsum += z;\nadd %eax,%ebx\n对于-fno-omit-frame-pointer:\n404,625,639个前端停顿周期,前端周期空闲率为0.00%\n10,514个缓存失效\n运行时间为0.375445137秒\n[...]\na.out中75.35%的时间在add(int const&, int const&) [clone .isra.0]函数中\na.out中24.46%的时间在work(int, int)函数中\n[...]\nint add(const int& x, const int& y)函数中:\npush %rbp\nlea (%rdi,%rsi,1),%eax\nint add(const int& x, const int& y)函数中:\nmov %rsp,%rbp\nreturn x + y;\npop %rbp\n[...]\nint z = add(x, y);\n? callq add(int const&, int const&) [clone .isra.0]\nsum += z;\nadd %eax,%ebx\n在慢速情况下,我们在调用add()时停顿。我检查了perf在我的机器上能够输出的所有内容;不仅仅是上面给出的统计数据。\n对于相同的可执行文件,stalled-cycles-frontend与执行时间呈线性相关关系;我没有注意到其他任何与执行时间如此明显相关的因素。(对比不同可执行文件的stalled-cycles-frontend对我来说没有意义。)\n我包括了缓存失效,因为它是第一条评论中提到的。我通过perf对我的机器上可以测量的所有缓存失效进行了检查,而不仅仅是上面给出的那些。缓存失效非常非常嘈杂,并且与执行时间几乎没有相关性。

0
0 Comments

为什么当我选择优化大小而不是速度时,GCC生成的代码比较快15-20%?这个问题的出现的原因是由于程序的对齐方式对于整体性能(包括大型程序)产生了影响。例如,这篇文章(也许这个版本也出现在CACM上)展示了链接顺序和操作系统环境大小的变化足以显著影响性能。他们将这归因于"热循环"的对齐方式。

这篇论文标题为"在没有做任何明显错误的情况下产生错误的数据!"表明由于几乎无法控制的程序运行环境的差异,意外的实验偏差可能使许多基准测试结果毫无意义。

我认为你遇到的是同样观察的不同角度。

对于性能关键的代码,这是一个为在安装或运行时评估环境并在不同优化版本的关键例程之间选择最佳本地版本的系统的很好的论点。

解决方法是为了性能关键的代码,应该在安装或运行时评估环境,并选择最佳的本地版本来进行优化。这样可以避免受到程序运行环境的影响,从而获得更好的性能。

0
0 Comments

GCC为什么在优化为大小而不是速度时能生成15-20%更快的代码?

这个问题的出现原因是对齐问题。对齐对性能有重要影响,这就是为什么我们首先有了“-falign-*”标志。GCC的默认行为是“默认将循环对齐到8字节,但如果不需要填充超过10字节,则尝试将其对齐到16字节。”然而,这个默认值在这种特殊情况和我的机器上并不是最佳选择。在这种情况下,Clang 3.4(trunk)使用“-O3”进行适当的对齐,生成的代码不会出现这种奇怪的行为。

如果做了不恰当的对齐,会使情况变得更糟。不必要或糟糕的对齐只会浪费字节,可能增加缓存未命中等问题。

解决方法是通过告诉GCC进行正确的对齐来解决。可以使用以下命令进行对齐:g++ -O2 -falign-functions=16 -falign-loops=16

长答案是,如果以下情况发生,代码运行速度会变慢:

- 一个XX字节的边界中间切断了add()函数。

- 如果调用add()的目标没有对齐,需要跳过一个XX字节的边界。

- add()函数没有对齐。

- 循环没有对齐。

如果以上情况发生,代码运行速度会变慢。在某些机器上,256字节的边界切断add()函数,导致代码变慢。在另一些机器上,循环和add()函数没有对齐,导致代码变慢。

如果在C或C++源代码中进行与堆栈对齐无关的微优化,可以通过使用正确的对齐告诉GCC来确保不会干扰这种意外的幸运/不幸的对齐。

解决这个问题的方法是在编译时使用适当的对齐标志,如g++ -O2 -falign-functions=16 -falign-loops=16。通过这种方式,可以确保函数和循环都得到正确的对齐,从而避免性能下降。

这个问题的出现是因为在优化为大小时,某些热点代码对对齐非常敏感,而优化为速度时,对齐不够好。这是一个偶然的幸运对齐而导致代码更快,与优化为大小无关。因此,以后在进行项目优化时需要注意对齐的影响。

此外,编译器可能无法内联一些较小的函数,如add()。这可能是因为编译器没有看到函数的定义,导致无法内联。使用LTO(链接时优化)可以解决这个问题,通过对整个二进制映像进行优化,编译器可以看到所有文件中的信息,从而能够正确地内联函数。

对齐问题可能会导致性能差异,因此在进行基准测试时需要注意。

0
0 Comments

GCC为了“平均”处理器进行优化,默认情况下编译器会优化代码。由于不同的处理器偏好不同的指令序列,启用了-O2的编译器优化可能会对平均处理器有益,但会降低您特定处理器的性能(-Os也是如此)。如果您在不同的处理器上尝试相同的示例,您会发现其中一些处理器受益于-O2,而其他处理器更适合-Os优化。

以下是在几个处理器上运行time ./test 0 0的结果(报告的用户时间):

AMD Opteron 8350:gcc-4.8.1优化选项为-O2的时间为0.704秒,优化选项为-Os的时间为0.896秒;

AMD FX-6300:gcc-4.8.1优化选项为-O2的时间为0.392秒,优化选项为-Os的时间为0.340秒;

AMD E2-1800:gcc-4.7.2优化选项为-O2的时间为0.740秒,优化选项为-Os的时间为0.832秒;

Intel Xeon E5405:gcc-4.8.1优化选项为-O2的时间为0.603秒,优化选项为-Os的时间为0.804秒;

Intel Xeon E5-2603:gcc-4.4.7优化选项为-O2的时间为1.121秒,优化选项为-Os的时间为1.122秒;

Intel Core i3-3217U:gcc-4.6.4优化选项为-O2的时间为0.709秒,优化选项为-Os的时间为0.709秒;

Intel Core i3-3217U:gcc-4.7.3优化选项为-O2的时间为0.708秒,优化选项为-Os的时间为0.822秒;

Intel Core i3-3217U:gcc-4.8.1优化选项为-O2的时间为0.708秒,优化选项为-Os的时间为0.944秒;

Intel Core i7-4770K:gcc-4.8.1优化选项为-O2的时间为0.296秒,优化选项为-Os的时间为0.288秒;

Intel Atom 330:gcc-4.8.1优化选项为-O2的时间为2.003秒,优化选项为-Os的时间为2.007秒;

ARM 1176JZF-S (Broadcom BCM2835):gcc-4.6.3优化选项为-O2的时间为3.470秒,优化选项为-Os的时间为3.480秒;

ARM Cortex-A8 (TI OMAP DM3730):gcc-4.6.3优化选项为-O2的时间为2.727秒,优化选项为-Os的时间为2.727秒;

ARM Cortex-A9 (TI OMAP 4460):gcc-4.6.3优化选项为-O2的时间为1.648秒,优化选项为-Os的时间为1.648秒;

ARM Cortex-A9 (Samsung Exynos 4412):gcc-4.6.3优化选项为-O2的时间为1.250秒,优化选项为-Os的时间为1.250秒;

ARM Cortex-A15 (Samsung Exynos 5250):gcc-4.7.2优化选项为-O2的时间为0.700秒,优化选项为-Os的时间为0.700秒;

Qualcomm Snapdragon APQ8060A:gcc-4.8优化选项为-O2的时间为1.53秒,优化选项为-Os的时间为1.52秒。

在某些情况下,您可以通过要求GCC优化为特定处理器(使用-mtune=native-march=native选项)来减轻不利优化的影响。

对于AMD FX-6300,使用gcc-4.8.1编译器和优化选项-mtune=native,时间为0.340秒,使用优化选项-Os -mtune=native,时间也为0.340秒。

对于AMD E2-1800,使用gcc-4.7.2编译器和优化选项-mtune=native,时间为0.740秒,使用优化选项-Os -mtune=native,时间为0.832秒。

对于Intel Xeon E5405,使用gcc-4.8.1编译器和优化选项-mtune=native,时间为0.603秒,使用优化选项-Os -mtune=native,时间为0.803秒。

对于Intel Core i7-4770K,使用gcc-4.8.1编译器和优化选项-mtune=native,时间为0.296秒,使用优化选项-Os -mtune=native,时间为0.288秒。

更新:在基于Ivy Bridge的Core i3上,gcc-4.6.4gcc-4.7.3gcc-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

根据测试结果,可以看出使用不同的优化选项和处理器的性能差异。您可以通过使用-fno-align-functions -fno-align-loops选项来减少不利优化的影响。对于AMD-FX 6300来说,使用-O2 -fno-align-functions -fno-align-loops将时间减少到0.340秒。

然而,对于其他处理器,最佳对齐方式是依赖于处理器的,因为某些处理器更倾向于对齐循环和函数。

需要注意的是,性能不仅取决于优化选项,还与处理器的特性和架构有关。因此,为了获得最佳性能,可能需要进行更多的测试和优化。

0