在编写优化的 ftol
函数时,我在 GCC 4.6.1
中发现了一些非常奇怪的行为。让我先向您展示代码(为了清楚起见,我标记了不同之处):
fast_trunc_one, C:
int fast_trunc_one(int i) {
int mantissa, exponent, sign, r;
mantissa = (i & 0x07fffff) | 0x800000;
exponent = 150 - ((i >> 23) & 0xff);
sign = i & 0x80000000;
if (exponent < 0) {
r = mantissa << -exponent; /* diff */
} else {
r = mantissa >> exponent; /* diff */
}
return (r ^ -sign) + sign; /* diff */
}
fast_trunc_two, C:
int fast_trunc_two(int i) {
int mantissa, exponent, sign, r;
mantissa = (i & 0x07fffff) | 0x800000;
exponent = 150 - ((i >> 23) & 0xff);
sign = i & 0x80000000;
if (exponent < 0) {
r = (mantissa << -exponent) ^ -sign; /* diff */
} else {
r = (mantissa >> exponent) ^ -sign; /* diff */
}
return r + sign; /* diff */
}
好像一样吧?好吧,海湾合作委员会不同意。使用 gcc -O3 -S -Wall -o test.s test.c
编译后,这是程序集输出:
fast_trunc_one,生成:
_fast_trunc_one:
LFB0:
.cfi_startproc
movl 4(%esp), %eax
movl $150, %ecx
movl %eax, %edx
andl $8388607, %edx
sarl $23, %eax
orl $8388608, %edx
andl $255, %eax
subl %eax, %ecx
movl %edx, %eax
sarl %cl, %eax
testl %ecx, %ecx
js L5
rep
ret
.p2align 4,,7
L5:
negl %ecx
movl %edx, %eax
sall %cl, %eax
ret
.cfi_endproc
fast_trunc_two,生成:
_fast_trunc_two:
LFB1:
.cfi_startproc
pushl %ebx
.cfi_def_cfa_offset 8
.cfi_offset 3, -8
movl 8(%esp), %eax
movl $150, %ecx
movl %eax, %ebx
movl %eax, %edx
sarl $23, %ebx
andl $8388607, %edx
andl $255, %ebx
orl $8388608, %edx
andl $-2147483648, %eax
subl %ebx, %ecx
js L9
sarl %cl, %edx
movl %eax, %ecx
negl %ecx
xorl %ecx, %edx
addl %edx, %eax
popl %ebx
.cfi_remember_state
.cfi_def_cfa_offset 4
.cfi_restore 3
ret
.p2align 4,,7
L9:
.cfi_restore_state
negl %ecx
sall %cl, %edx
movl %eax, %ecx
negl %ecx
xorl %ecx, %edx
addl %edx, %eax
popl %ebx
.cfi_restore 3
.cfi_def_cfa_offset 4
ret
.cfi_endproc
这是一个极端的区别。这实际上也显示在配置文件中,fast_trunc_one
比 fast_trunc_two
快大约 30%。现在我的问题是:造成这种情况的原因是什么?
最佳答案
已更新以与 OP 的编辑同步
通过修改代码,我设法了解了 GCC 如何优化第一种情况。
在理解它们为何如此不同之前,首先我们必须了解 GCC 如何优化 fast_trunc_one()
。
信不信由你,fast_trunc_one()
正在为此优化:
int fast_trunc_one(int i) {
int mantissa, exponent;
mantissa = (i & 0x07fffff) | 0x800000;
exponent = 150 - ((i >> 23) & 0xff);
if (exponent < 0) {
return (mantissa << -exponent); /* diff */
} else {
return (mantissa >> exponent); /* diff */
}
}
这会生成与原始 fast_trunc_one()
完全相同的程序集 - 注册名称和所有内容。
请注意,fast_trunc_one()
的程序集中没有xor
。这就是它送给我的原因。
怎么会这样?
第 1 步: sign = -sign
首先,让我们看一下sign
变量。由于 sign = i & 0x80000000;
,sign
只能取两个可能的值:
符号 = 0
sign = 0x80000000
现在认识到在这两种情况下,sign == -sign
。因此,当我将原始代码更改为:
int fast_trunc_one(int i) {
int mantissa, exponent, sign, r;
mantissa = (i & 0x07fffff) | 0x800000;
exponent = 150 - ((i >> 23) & 0xff);
sign = i & 0x80000000;
if (exponent < 0) {
r = mantissa << -exponent;
} else {
r = mantissa >> exponent;
}
return (r ^ sign) + sign;
}
它生成与原始 fast_trunc_one()
完全相同的程序集。我将为您省去程序集,但它是相同的 - 注册名称和所有内容。
第 2 步: 数学简化:x + (y ^ x) = y
sign
只能取两个值之一,0
或 0x80000000
。
- 当
x = 0
时,则x + (y ^ x) = y
那么平凡成立。 0x80000000
的加法和异或操作是一样的。它翻转符号位。因此x + (y ^ x) = y
在x = 0x80000000
时也成立。
因此,x + (y ^ x)
简化为 y
。代码简化为:
int fast_trunc_one(int i) {
int mantissa, exponent, sign, r;
mantissa = (i & 0x07fffff) | 0x800000;
exponent = 150 - ((i >> 23) & 0xff);
sign = i & 0x80000000;
if (exponent < 0) {
r = (mantissa << -exponent);
} else {
r = (mantissa >> exponent);
}
return r;
}
同样,这会编译成完全相同的程序集 - 注册名称和所有内容。
上面的版本最终简化为:
int fast_trunc_one(int i) {
int mantissa, exponent;
mantissa = (i & 0x07fffff) | 0x800000;
exponent = 150 - ((i >> 23) & 0xff);
if (exponent < 0) {
return (mantissa << -exponent); /* diff */
} else {
return (mantissa >> exponent); /* diff */
}
}
这几乎就是 GCC 在程序集中生成的内容。
那么为什么编译器不将 fast_trunc_two()
优化为相同的东西?
fast_trunc_one()
的关键部分是 x + (y ^ x) = y
优化。在 fast_trunc_two()
中,x + (y ^ x)
表达式正在跨分支拆分。
我怀疑这可能足以让 GCC 无法进行此优化。 (它需要将 ^ -sign
提升出分支并将其合并到末尾的 r + sign
中。)
例如,这会生成与 fast_trunc_one()
相同的程序集:
int fast_trunc_two(int i) {
int mantissa, exponent, sign, r;
mantissa = (i & 0x07fffff) | 0x800000;
exponent = 150 - ((i >> 23) & 0xff);
sign = i & 0x80000000;
if (exponent < 0) {
r = ((mantissa << -exponent) ^ -sign) + sign; /* diff */
} else {
r = ((mantissa >> exponent) ^ -sign) + sign; /* diff */
}
return r; /* diff */
}
关于c - 为什么 GCC 会为几乎相同的 C 代码生成如此截然不同的程序集?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/10250419/