当前位置: 首页 > 知识库问答 >
问题:

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

訾高明
2023-03-14

我第一次注意到是在2009年,如果我针对大小(-OS)而不是速度(-O2-O3)进行优化,GCC(至少在我的项目和机器上)有生成明显更快代码的趋势,从那以后我一直在想为什么。

我已经设法创建了(相当愚蠢的)代码,它显示了这种令人惊讶的行为,并且足够小,可以在这里发布。

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 s,如果用-o2-o3编译,则需要0.44 。这些时间是一致的,并且几乎没有噪声(GCC4.7.2,x86_64 GNU/Linux,Intel Core i5-3320M)。

(更新:我已经将所有汇编代码移到了GitHub:它们使文章变得臃肿,显然对问题增加的价值很小,因为fno-align-*标志具有相同的效果。)

下面是使用-os-o2生成的程序集。

不幸的是,我对程序集的理解非常有限,因此我不知道我接下来所做的是否正确:我抓取了-o2的程序集,并将它的所有差异合并到-os的程序集中,除了.P2Align行,这里的结果。这段代码仍然在0.38s内运行,唯一的区别是.P2align内容。

如果我没猜错的话,这些是用于堆栈对齐的padding。根据为什么GCC pad与NOPS一起工作?这样做是希望代码运行得更快,但显然这种优化在我的情况下适得其反。

是不是填充物才是本案的罪魁祸首?为什么和如何?

它产生的噪音几乎使定时微优化变得不可能。

当我对C或C++源代码进行微优化(与堆栈对齐无关)时,如何确保这种偶然的幸运/不幸运的对齐没有干扰?

更新:

在Pascal Cuoq的回答之后,我稍微修改了一下排列。通过将-o2-fno-align-functions-fno-align-loops传递给gcc,程序集中的所有.P2align都将消失,生成的可执行文件将在0.38s内运行。根据gcc文档:

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

  -falign-functions  -falign-jumps  -falign-loops
  -falign-labels  -freorder-blocks  -freorder-blocks-and-partition
  -fprefetch-loop-arrays

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

我仍然对马拉特·杜汗在回答中建议的-march=native持怀疑态度。我不相信它不仅仅是在干扰这个(错位)对齐的问题;对我的机器绝对没有影响。(尽管如此,我还是推翻了他的回答。)

更新2:

我们可以将-os排除在外。下面的时间是通过编译获得的

>

  • -o2-fno-omit-frame-pointer0.37s

    -O2-fno-align-functions-fno-align-loops0.37s

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

    -O20.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对我来说没有意义。)

    我在第一条评论中包含了缓存缺失。我检查了在我的机器上可以通过perf测量的所有缓存未命中,而不仅仅是上面给出的那些。高速缓存的丢失是非常非常嘈杂的,并且与执行时间的相关性很小,甚至没有相关性。

  • 共有2个答案

    贺光华
    2023-03-14

    我的同事帮我为我的问题找到了一个似乎合理的答案。他注意到256字节边界的重要性。他没有在这里注册,并鼓励我自己贴出答案(并拿走所有的名声)。

    简短回答:

    是不是填充物才是本案的罪魁祸首?为什么和如何?

    这一切都归结为对齐。对齐会对性能产生重大影响,这就是为什么我们首先使用-falign-*标志的原因。

    我提交了一份(伪造的?)向gcc开发人员报告错误。结果表明,默认的行为是“默认情况下,我们将循环对齐到8个字节,但如果不需要填充超过10个字节,则尝试将其对齐到16个字节。”显然,在此特定情况下,在我的机器上,此默认值不是最佳选择。带有-o3的CLANG3.4(trunk)会进行适当的对齐,生成的代码不会显示这种奇怪的行为。

    当然,如果做了不恰当的对齐,那会使事情变得更糟。不必要的/错误的对齐只会无缘无故地消耗字节,并可能增加缓存丢失等。

    它产生的噪音几乎使定时微优化变得不可能。

    当我对C或C++源代码进行微优化(与堆栈对齐无关)时,如何确保这种偶然的幸运/不幸运的对齐没有干扰?

    只需告诉gcc进行正确的对齐:

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

    长长的回答:

    如果出现以下情况,代码将运行得更慢:

    >

  • xx字节边界在中间切割add()(xx与计算机相关)。

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

    如果add()未对齐。

    如果循环未对齐。

    前两个是美丽的可见的代码和结果,马拉特杜汗友好地张贴。在本例中,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()

    仅此而已,这是很容易无意地创建热点,就像在操作中的一个。当然,这部分是我的错:gcc是一个出色的编译器。如果将上面的代码编译为:g++-o2-flto add.cpp main.cpp,也就是说,如果我执行链接时间优化,代码运行时间为0.19s!

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

  • 归翔
    2023-03-14

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

    以下是几个处理器上时间./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
    
     类似资料:
    • 这是用构建的好例子吗?有理由不这样做吗?我从未真正见过一个程序,无论它花多少时间在I/O上,都是为了大小而编译的。

    • 问题内容: 没有迅速的方法。该程序必须从某处开始执行。那么快速代码执行的切入点是什么,它是如何确定的? 问题答案: 普通Swift模块中的入口点是模块中名为的文件。是唯一一个允许在顶层具有表达式和语句的文件(模块中的所有其他Swift文件只能包含声明)。 可可触摸使用属性上的实现,而不是一个纪念的入口点文件。可可曾经使用了一个简单的最小文件,但是从Xcode 6.1开始, 它在的实现上使用属性。

    • 从我的大学课程中,我听说,按照惯例,最好将更可能的条件放在中,而不是条件中,这可能有助于静态分支预测器。例如: 可以改写为: 我找到了一篇博客文章分支模式,使用GCC,它更详细地解释了这种现象: 为 if 语句生成转发分支。使它们不太可能被采用的理由是,处理器可以利用这样一个事实,即分支指令之后的指令可能已经放置在指令单元内的指令缓冲区中。 旁边写着(强调我的): 在编写if-else语句时,始终

    • (这个问题与此密切相关,但它是一个更具体的问题,我希望能就此得到答案)

    • 我正在使用GCC为ARM开发一个C。我遇到了一个问题,我没有启用优化,我无法为我的代码创建二进制(ELF),因为它不适合可用空间。然而,如果我只是启用调试优化(-Og),这是我所知的最低优化,代码很容易适合。 在这两种情况下,都启用了-ffunction-节、-fdata-节、-fno-异常和-Wl、--gc-节。 闪存大小:512 kB 没有优化:. text溢出约200 kB 使用-Og优化:

    • 这是一个使用ValArray的简单c程序: 如果我像这样编译并运行它: 产出如预期: 但是,如果我像这样编译和运行它: 输出为: 如果使用优化参数,也会发生同样的情况。 GCC版本是(Archlinux最新版本): 但是,如果我尝试叮当,两者 和 产生相同的正确结果: clang版本是: 我还尝试了在Debian上使用GCC 4.9.2,其中可执行文件会产生正确的结果。 这是GCC中可能存在的错误