【发布时间】: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++