在不同优化级别访问gcc/g++中的局部变量与全局变量的速度

Speed of accessing local vs. global variables in gcc/g++ at different optimization levels

本文关键字:g++ 局部变量 速度 全局变量 gcc 优化 访问      更新时间:2023-10-16

我发现,当访问循环中的局部或全局变量时,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左右(。

另一方面,在以下代码片段中,起始点指向大小为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的前两个片段生成的程序集,在这种情况下差异最大。据我所知,这看起来像是编译器中的一个错误:0x3b9aca00是十六进制的SIZE,0x80496dc必须是全局的地址。我用一个新的编译器检查过,这种情况不再发生了。然而,第二对片段中的差异是相似的。

    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    

最后是剩余片段的代码,现在由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

谢谢。

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

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

最后,请注意,如果指针p是另一种类型,则编译器可以调用"严格别名规则"进行优化,而不管它无法推断p不指向global。但是,由于char类型的lvalues通常用于观察其他类型的表示,因此允许使用这种别名,并且编译器不能在您的示例中使用这种快捷方式。

全局变量=全局内存,并且会出现混叠(读作:对优化器不利--在最坏的情况下必须读取-修改-写入(。

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

访问寄存器的顺序为单个周期,访问内存的顺序为15-1000个周期(取决于高速缓存行是否在高速缓存中且未被另一个核心无效,以及取决于页面是否在TLB中(。