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

我在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-jumps  -falign-loops
-falign-labels  -freorder-blocks  -freorder-blocks-and-partition
-fprefetch-loop-arrays

所以,这似乎是一个(错误的)对齐问题。

我仍然怀疑在Marat Dukhan的回答中建议的-march=native。我不相信它不只是干扰这个(错误的)对齐问题;这对我的机器完全没有影响。(不过,我还是对他的回答投了赞成票。)


更新2:

我们可以把-Os排除在外。通过编译

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

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

  • -S -O2然后手动移动add()的组装后,work() 0.37秒

  • -O2 0.44秒

在我看来,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对我来说没有意义。)

我在第一个注释出现时就包含了缓存错误。我检查了我的机器上可以通过perf测量的所有缓存失误,而不仅仅是上面给出的那些。缓存丢失是非常非常嘈杂的,并且与执行时间几乎没有相关性。

116821 次浏览

我认为你可以得到与你所做的相同的结果:

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

…通过使用-O2 -falign-functions=1 -falign-jumps=1 -falign-loops=1 -falign-labels=1。15年来,我一直在用这些选项编译所有东西,每次我费心测量时,它们都比普通的-O2快。

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

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

不,这与堆栈无关,默认生成的nop和options -falign-*=1 prevent用于代码对齐。

根据为什么GCC垫功能与NOPs?这样做是希望代码运行得更快,但显然这种优化在我的情况下适得其反。

在这种情况下,填充物是罪魁祸首吗?为什么?怎么做?

衬垫很可能是罪魁祸首。填充被认为是必要的,在某些情况下是有用的,因为代码通常以16字节为一行获取(详细信息参见Agner Fog的优化资源,因处理器型号而异)。将函数、循环或标签对齐在16字节的边界上意味着,从统计上看,需要少一行来包含函数或循环的机会增加了。显然,它会适得其反,因为这些nop降低了代码密度,从而降低了缓存效率。在循环和标签的情况下,nop甚至可能需要执行一次(当执行正常到达循环/标签时,而不是从跳转开始)。

默认情况下,编译器针对“普通”处理器进行优化。由于不同的处理器支持不同的指令序列,由-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.44.7.34.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

我绝不是这方面的专家,但我似乎记得,当涉及到分支预测时,现代处理器相当敏感。用于预测分支的算法(至少在我编写汇编程序代码时是这样)基于代码的几个属性,包括目标的距离和方向。

我想到的场景是小循环。当分支向后走并且距离不是太远时,分支预测就会针对这种情况进行优化,因为所有的小循环都是这样完成的。当你在生成的代码中交换addwork的位置时,或者当两者的位置发生轻微变化时,同样的规则可能会起作用。

也就是说,我不知道如何验证这一点,我只是想让你知道,这可能是你想要研究的东西。

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


简短的回答:

在这种情况下,填充物是罪魁祸首吗?为什么?怎么做?

这一切都归结为结盟。 Alignments可以对性能产生重大影响,这就是为什么我们在第一个地方有-falign-*标志。

我已经提交了给GCC开发人员的(虚假的?)错误报告。事实证明,默认行为是“我们默认将循环对齐为8字节,但如果我们不需要填充超过10个字节,则尝试将其对齐为16字节。”显然,在这种特殊情况下,在我的机器上,这种默认不是最佳选择。带有-O3的Clang 3.4 (trunk)做了适当的对齐,生成的代码没有显示这种奇怪的行为。

当然,如果对齐不当,情况会变得更糟。一个不必要的/糟糕的对齐只会毫无理由地消耗字节,并潜在地增加缓存丢失等。

它产生的噪音使时间进行了微优化 不可能。< / p >

我怎么能确保这种偶然的幸运/不幸的对齐 当我做微优化(与堆栈无关 对齐)的C或c++源代码?< / p >

简单地告诉gcc做正确的对齐:

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


长一点的回答:

如果出现以下情况,代码运行速度会变慢:

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

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

  • 如果add()没有对齐。

  • 如果循环没有对齐。

前两个在Marat Dukhan好心发帖. Marat Dukhan好心发帖. Marat Dukhan好心发帖. Marat Dukhan好心发帖. Marat Dukhan好心发帖. Marat Dukhan好心发帖. Marat Dukhan好心发帖. 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-omit-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倍)。

如果您的程序受CODE L1缓存的限制,那么针对大小进行优化就会突然开始发挥作用。

当我最后检查时,编译器还不够聪明,不能在所有情况下都解决这个问题。

在您的例子中,-O3可能生成的代码足够用于两条缓存线,但-Os适合用于一条缓存线。

我添加这篇文章是为了指出,对齐对程序(包括大型程序)整体性能的影响已经被研究过了。例如,这篇文章(我相信它的一个版本也出现在CACM中)显示了链接顺序和操作系统环境大小的变化如何足以显著改变性能。他们将此归因于“热循环”的一致性。

这篇题为“在没有做任何明显错误的事情的情况下产生错误的数据!”的论文说,由于程序运行环境中几乎不可控的差异而导致的无意的实验偏差可能会导致许多基准测试结果毫无意义。

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

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