在不同优化级别以gcc / g ++访问本地变量和全局变量的速度

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

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

它使用全局变量long global ,vs.

  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且具有两个单核处理器和4GB RAM的计算机上使用gcc / g ++版本3.4.5。 我用于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是hex的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 : 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  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 : 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  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不会使用寄存器来优化全局版本。 这只是一个缺乏function,还是会导致可执行文件的不可接受的行为?

  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 : 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  804849d: 5d pop %ebp 804849e: c3 ret 804849f: 90 nop 080484a0 : 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  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 ,并且编译器可以相应地进行优化。 除非它是static ,否则推断全局变量global不被指向要困难得多,因为该全局变量的地址可以在另一个编译单元中获取并传递。

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

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

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

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

访问寄存器的顺序是一个周期,访问内存大约为15-1000个周期(取决于缓存行是否在缓存中而不是由另一个内核无效,并且取决于页面是否在TLB中) )。