【问题标题】:Speed of accessing local vs. global variables in gcc/g++ at different optimization levels在不同优化级别访问 gcc/g++ 中的局部变量和全局变量的速度
【发布时间】:2011-11-06 15:34:46
【问题描述】:

我发现 gcc 中不同的编译器优化级别在访问循环中的局部变量或全局变量时会给出完全不同的结果。这让我感到惊讶的原因是,如果访问一种类型的变量比访问另一种类型的变量更可优化,我认为 gcc 优化会利用这一事实。 这里有两个例子(在 C++ 中,但它们的 C 对应物给出了几乎相同的时间):

    global = 0;
    for (int i = 0; i < SIZE; i++)
        global++;

使用全局变量long global,对比

    long tmp = 0;
    for (int i = 0; i < SIZE; i++)
        tmp++;
    global = tmp;

在优化级别 -O0 时间基本上是相等的(正如我所期望的),在 -O1 它稍微快一些但仍然相等,但是从 -O2 开始,使用全局变量的版本要快得多(大约 7 个因子)。

另一方面,在以下代码片段中,start 指向一个大小为 SIZE 的字节块:

    global = 0;
    for (const char* p = start; p < start + SIZE; p++)
        global += *p;

    long tmp = 0;
    for (const char* p = start; p < start + SIZE; p++)
        tmp += *p;
    global = tmp;

在 -O0 处,时间很接近,尽管使用局部变量的版本稍微快一些,这似乎并不令人惊讶,因为它可能会存储在寄存器中,而 global 不会。然后在 -O1 和更高版本中,使用局部变量的版本要快得多(超过 50% 或 1.5 倍)。如前所述,这让我感到惊讶,因为我认为对于 gcc 来说,稍后使用局部变量(在生成的优化代码中)分配给全局变量就像我一样容易。

所以我的问题是:全局变量和局部变量是什么使得 gcc 只能对一种类型执行某些优化,而不能对另一种类型执行某些优化?

一些可能相关或不相关的细节:我在一台运行 RHEL4 的机器上使用了 gcc/g++ 版本 3.4.5,该机器具有两个单核处理器和 4GB RAM。我用于 SIZE(预处理器宏)的值为 1000000000。第二个示例中的字节块是动态分配的。

以下是优化级别 0 到 4 的一些时序输出(与上面的顺序相同):

$ ./st0
Result using global variable: 1000000000 in 2.213 seconds.
Result using local variable:  1000000000 in 2.210 seconds.
Result using global variable: 0 in 3.924 seconds.
Result using local variable:  0 in 3.710 seconds.
$ ./st1
Result using global variable: 1000000000 in 0.947 seconds.
Result using local variable:  1000000000 in 0.947 seconds.
Result using global variable: 0 in 2.135 seconds.
Result using local variable:  0 in 1.212 seconds.
$ ./st2
Result using global variable: 1000000000 in 0.022 seconds.
Result using local variable:  1000000000 in 0.552 seconds.
Result using global variable: 0 in 2.135 seconds.
Result using local variable:  0 in 1.227 seconds.
$ ./st3
Result using global variable: 1000000000 in 0.065 seconds.
Result using local variable:  1000000000 in 0.461 seconds.
Result using global variable: 0 in 2.453 seconds.
Result using local variable:  0 in 1.646 seconds.
$ ./st4
Result using global variable: 1000000000 in 0.063 seconds.
Result using local variable:  1000000000 in 0.468 seconds.
Result using global variable: 0 in 2.467 seconds.
Result using local variable:  0 in 1.663 seconds.

编辑 这是前两个带有开关 -O2 的 sn-ps 生成的程序集,差异最大的情况。据我了解,它看起来像是编译器中的一个错误:0x3b9aca00 是十六进制的 SIZE,0x80496dc 必须是全局地址。 我检查了一个更新的编译器,这不再发生了。但是第二对 sn-ps 的区别是相似的。

    void global1()
    {
        int i;
        global = 0;
        for (i = 0; i < SIZE; i++)
            global++;
    }

    void local1()
    {
        int i;
        long tmp = 0;
        for (i = 0; i < SIZE; i++)
            tmp++;
        global = tmp;
    }

    080483d0 <global1>:
     80483d0:   55                      push   %ebp
     80483d1:   89 e5                   mov    %esp,%ebp
     80483d3:   c7 05 dc 96 04 08 00    movl   $0x0,0x80496dc
     80483da:   00 00 00 
     80483dd:   b8 ff c9 9a 3b          mov    $0x3b9ac9ff,%eax
     80483e2:   89 f6                   mov    %esi,%esi
     80483e4:   83 e8 19                sub    $0x19,%eax
     80483e7:   79 fb                   jns    80483e4 <global1+0x14>
     80483e9:   c7 05 dc 96 04 08 00    movl   $0x3b9aca00,0x80496dc
     80483f0:   ca 9a 3b 
     80483f3:   c9                      leave  
     80483f4:   c3                      ret    
     80483f5:   8d 76 00                lea    0x0(%esi),%esi

    080483f8 <local1>:
     80483f8:   55                      push   %ebp
     80483f9:   89 e5                   mov    %esp,%ebp
     80483fb:   b8 ff c9 9a 3b          mov    $0x3b9ac9ff,%eax
     8048400:   48                      dec    %eax
     8048401:   79 fd                   jns    8048400 <local1+0x8>
     8048403:   c7 05 dc 96 04 08 00    movl   $0x3b9aca00,0x80496dc
     804840a:   ca 9a 3b 
     804840d:   c9                      leave  
     804840e:   c3                      ret    
     804840f:   90                      nop    

最后这里是剩下的 sn-ps 的代码,现在由 gcc 4.3.3 使用 -O3 生成(虽然旧版本似乎生成了类似的代码)。看起来确实 global2(..) 编译为在循环的每次迭代中访问全局内存位置的函数,其中 local2(..) 使用寄存器。我仍然不清楚为什么 gcc 无论如何都不会使用寄存器来优化全局版本。这只是一个缺少的功能,还是真的会导致可执行文件的行为不可接受?

    void global2(const char* start)
    {
        const char* p;
        global = 0;
        for (p = start; p < start + SIZE; p++)
            global += *p;
    }

    void local2(const char* start)
    {
        const char* p;
        long tmp = 0;
        for (p = start; p < start + SIZE; p++)
            tmp += *p;
        global = tmp;
    }

    08048470 <global2>:
     8048470:   55                      push   %ebp
     8048471:   31 d2                   xor    %edx,%edx
     8048473:   89 e5                   mov    %esp,%ebp
     8048475:   8b 4d 08                mov    0x8(%ebp),%ecx
     8048478:   c7 05 24 a0 04 08 00    movl   $0x0,0x804a024
     804847f:   00 00 00 
     8048482:   8d b6 00 00 00 00       lea    0x0(%esi),%esi
     8048488:   0f be 04 11             movsbl (%ecx,%edx,1),%eax
     804848c:   83 c2 01                add    $0x1,%edx
     804848f:   01 05 24 a0 04 08       add    %eax,0x804a024
     8048495:   81 fa 00 ca 9a 3b       cmp    $0x3b9aca00,%edx
     804849b:   75 eb                   jne    8048488 <global2+0x18>
     804849d:   5d                      pop    %ebp
     804849e:   c3                      ret    
     804849f:   90                      nop    

    080484a0 <local2>:
     80484a0:   55                      push   %ebp
     80484a1:   31 c9                   xor    %ecx,%ecx
     80484a3:   89 e5                   mov    %esp,%ebp
     80484a5:   31 d2                   xor    %edx,%edx
     80484a7:   53                      push   %ebx
     80484a8:   8b 5d 08                mov    0x8(%ebp),%ebx
     80484ab:   90                      nop    
     80484ac:   8d 74 26 00             lea    0x0(%esi,%eiz,1),%esi
     80484b0:   0f be 04 13             movsbl (%ebx,%edx,1),%eax
     80484b4:   83 c2 01                add    $0x1,%edx
     80484b7:   01 c1                   add    %eax,%ecx
     80484b9:   81 fa 00 ca 9a 3b       cmp    $0x3b9aca00,%edx
     80484bf:   75 ef                   jne    80484b0 <local2+0x10>
     80484c1:   5b                      pop    %ebx
     80484c2:   89 0d 24 a0 04 08       mov    %ecx,0x804a024
     80484c8:   5d                      pop    %ebp
     80484c9:   c3                      ret    
     80484ca:   8d b6 00 00 00 00       lea    0x0(%esi),%esi

谢谢。

【问题讨论】:

  • 您确定这是该网站的正确用户名吗?
  • 请注意,当 SIZE 和 global 为非易失性时,编译器可能能够将整个循环更改为单个 global += SIZE;陈述。使用一个古老的 gcc 和只有 4MB 的内存让我想知道你在什么硬件上运行它,因为任何答案都很大程度上取决于硬件和其他可能的东西。也就是说,您可能更愿意自己分析生成的汇编代码,可能使用一个好的汇编级别分析器。
  • 一个有用的做法是获取编译器在每种情况下生成的机器代码(使用objdump 或类似的),并将其添加到您的问题中。
  • @doetoe:看看汇编器的输出做了什么。另请注意,任何“可以优化”都不等于“保证优化”,尤其是对于这些古老的编译器。要知道发生了什么,最好的办法就是看看发生了什么。
  • 代码肯定不是最优的,但这不是编译器中的错误。编译器不断发展,特别是当使用古老的编译器作为 gcc 3.x 时,可以预料事情不会优化得那么好。我们目前在 4.6.1 ...

标签: c++ c optimization gcc g++


【解决方案1】:

地址未被取到的局部变量tmp不能被指针p指向,编译器可以做相应的优化。要推断全局变量 global 没有被指向要困难得多,除非它是 static,因为该全局变量的地址可以在另一个编译单元中获取并传递。

如果读取程序集表明编译器强制自己从内存加载的频率比您预期的要高,并且您知道它担心的别名在实践中不存在,您可以通过将全局变量复制到本地来帮助它函数顶部的变量,并且在函数的其余部分仅使用局部变量。

最后,请注意,如果指针 p 是另一种类型,则编译器可以调用“严格别名规则”进行优化,而不管它无法推断出 p 不指向 global。但是因为char类型的左值经常被用来观察其他类型的表示,所以这种别名是有余地的,编译器在你的例子中不能采用这种捷径。

【讨论】:

  • 听起来很有道理。但我想这取决于start 是什么。
  • @Oli Charlesworth 与您评论的版本相比,我已将句子更改为“很难推断出未指向全局变量global”。正如您所说,这可能取决于 start 是什么。但是,确定*p 是否与global 别名的问题通常非常困难,以至于编译器可能甚至不会尝试(而“不采用地址的局部变量”标准是句法的、局部的并且在实践中运行良好)。
  • 感谢您的回答。但是,当全局变量是非易失性时,不只是其他线程可以在执行时更改值吗?提供线程安全难道不是程序员(而不是编译器)的责任吗?
  • @doetoe:问题是全局变量可以通过指针间接操作。如果编译器可以证明没有通过可能为全局别名的指针进行写入,它仍然可以优化访问。但这是一个很大的如果
  • 这个答案完全正确。就编译器所知,您可以调用global2,而&amp;global 位于startstart+SIZE 之间。所以编译器必须为每次循环迭代发出内存访问。使用char * 使得这特别难以优化,因为它可以为任何东西起别名...尝试编写一个类似的循环,其中*startglobal 是不同类型的对象,然后编译器可以很容易地证明指针没有别名。
【解决方案2】:

全局变量 = 全局内存,并受到别名的影响(读作:对优化器不利——在最坏的情况下必须读取-修改-写入)。

局部变量 = 寄存器(除非编译器真的帮不上忙,有时它也必须放入堆栈,但堆栈实际上保证在 L1 中)

访问一个寄存器是单周期的量级,访问内存是15-1000个周期的量级(取决于缓存行是否在缓存中并且没有被另一个内核失效,并且取决于页面是否在在 TLB 中)。

【讨论】:

  • 听起来很有道理。但我想这取决于start 是什么。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-01-25
  • 2021-01-19
  • 2021-12-10
  • 2017-09-20
  • 1970-01-01
相关资源
最近更新 更多