[英]GCC left shift overflow
下面这个小程序在 Mac 上使用 GCC 版本 4.2.1 (Apple Inc. build 5664) 非常笨拙。
#include <stdio.h>
int main(){
int x = 1 << 32;
int y = 32;
int z = 1 << y;
printf("x:%d, z: %d\n", x, z);
}
结果是x:0, z: 1
。
知道为什么 x 和 z 的值不同吗?
非常感谢。
简短回答:英特尔处理器将移位计数屏蔽为 5 位(最多 31 位)。 换句话说,实际执行的移位是 32 & 31,即 0(无变化)。
在 Linux 32 位 PC 上使用 gcc 会出现相同的结果。
我组装了这个程序的一个较短版本,因为我对为什么 32 位左移应该导致一个非零值感到困惑:
int main(){
int y = 32;
unsigned int z = 1 << y;
unsigned int k = 1;
k <<= y;
printf("z: %u, k: %u\n", z, k);
}
..使用命令gcc -Wall -o as -S deleteme.c
(评论是我自己的)
main:
leal 4(%esp), %ecx
andl $-16, %esp
pushl -4(%ecx)
pushl %ebp
movl %esp, %ebp
pushl %ecx
subl $36, %esp
movl $32, -16(%ebp) ; y = 32
movl -16(%ebp), %ecx ; 32 in CX register
movl $1, %eax ; AX = 1
sall %cl, %eax ; AX <<= 32(32)
movl %eax, -12(%ebp) ; z = AX
movl $1, -8(%ebp) ; k = 1
movl -16(%ebp), %ecx ; CX = y = 32
sall %cl, -8(%ebp) ; k <<= CX(32)
movl -8(%ebp), %eax ; AX = k
movl %eax, 8(%esp)
movl -12(%ebp), %eax
movl %eax, 4(%esp)
movl $.LC0, (%esp)
call printf
addl $36, %esp
popl %ecx
popl %ebp
leal -4(%ecx), %esp
ret
好的,这是什么意思? 正是这个指令让我感到困惑:
sall %cl, -8(%ebp) ; k <<= CX(32)
显然k被移位由32位左。
你明白了 - 它使用的是sall
指令,它是一个算术移位。 我不知道为什么将其旋转 32 会导致该位重新出现在初始位置。 我最初的猜测是处理器经过优化,可以在一个时钟周期内执行这条指令——这意味着任何超过 31 的移位都将被视为无关紧要。 但是我很想找到这个问题的答案,因为我希望旋转应该导致所有位都从数据类型的左端脱落。
我找到了一个指向http://faydoc.tripod.com/cpu/sal.htm的链接,它解释了移位计数(在 CL 寄存器中)被屏蔽为 5 位。 这意味着如果您尝试移动 32 位,实际执行的移动将是零位(即没有变化)。 答案来了!
如果您的ints
是 32 位或更短,则行为未定义......并且无法解释未定义的行为。
标准说:
6.5.7/3 [...] 如果右操作数的值为负或大于或等于提升的左操作数的宽度,则行为未定义。
您可以检查您的int
宽度位大小,例如:
#include <limits.h>
#include <stdio.h>
int main(void) {
printf("bits in an int: %d\n", CHAR_BIT * (int)sizeof (int));
return 0;
}
你可以检查你的int
宽度(可以有填充位),例如:
#include <limits.h>
#include <stdio.h>
int main(void) {
int width = 0;
int tmp = INT_MAX;
while (tmp) {
tmp >>= 1;
width++;
}
printf("width of an int: %d\n", width + 1 /* for the sign bit */);
return 0;
}
标准 6.2.6.2/2:对于有符号整数类型,对象表示的位应分为三组:值位、填充位和符号位。 不需要任何填充位; 应该只有一个符号位
C99 标准说,将数字移位操作数的位(或更多)宽度的结果是未定义的。 为什么?
好吧,这允许编译器为特定架构创建最有效的代码。 例如,i386 移位指令使用 5 位宽的字段来表示将 32 位操作数移位的位数。 C99 标准允许编译器简单地获取移位计数的底部五位并将它们放入字段中。 显然,这意味着 32 位的移位(= 100000 二进制)因此与 0 的移位相同,因此结果将是左操作数不变。
不同的 CPU 架构可能使用更宽的位域,比如 32 位。 编译器仍然可以将移位计数直接放在字段中,但这次结果将为 0,因为 32 位的移位会将所有位移出左操作数。
如果 C99 将这些行为中的一个或其他定义为正确,则 Intel 的编译器必须对过大的移位计数进行特殊检查,或者非 i386 的编译器必须屏蔽移位计数。
原因
int x = 1 << 32;
和
int z = 1 << y;
给出不同的结果是因为第一次计算是一个常量表达式,完全可以由编译器执行。 编译器必须使用 64 位算术计算常量表达式。 第二个表达式由编译器生成的代码计算。 由于 y 和 z 的类型都是int
因此代码使用 32 位宽的 int 生成计算(int 在 i386 和 x86_64 上都是 32 位,在 Apple 上使用 gcc)。
在我看来“int x = y << 32;” 如果 sizeof(int)==4 没有意义。
但我有一个类似的问题:
长 y = ... 长 x = y << 32;
我在那里收到警告“警告:左移计数> = 类型宽度”,即使 sizeof(long) 在相关目标上为 8。 我通过这样做来摆脱警告:
长 x = (y << 16) << 16;
这似乎奏效了。
在 64 位架构上没有警告。 在 32 位架构上有。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.