为什么在移位32位值时仅使用移位操作数的低5位? (例如(UInt32)1 << 33 == 2)

请考虑以下代码:

UInt32 val = 1; UInt32 shift31 = val << 31; // shift31 == 0x80000000 UInt32 shift32 = val << 32; // shift32 == 0x00000001 UInt32 shift33 = val << 33; // shift33 == 0x00000002 UInt32 shift33a = (UInt32)((UInt64)val << 33); // shift33a == 0x00000000 

它不会生成警告(关于使用大于32的class次),因此它必须是预期的行为。

实际被放到生成的程序集中的代码(或者至少是Reflector对代码的解释)是

  uint val = 1; uint shift31 = val << 0x1f; uint shift32 = val; uint shift33 = val << 1; uint shift33a = val << 0x21; 

IL(再次,使用Reflector)是

 L_0000: nop L_0001: ldc.i4.1 L_0002: stloc.0 L_0003: ldloc.0 L_0004: ldc.i4.s 0x1f L_0006: shl L_0007: stloc.1 L_0008: ldloc.0 L_0009: stloc.2 L_000a: ldloc.0 L_000b: ldc.i4.1 L_000c: shl L_000d: stloc.3 L_000e: ldloc.0 L_000f: conv.u8 L_0010: ldc.i4.s 0x21 L_0012: shl L_0013: conv.u4 L_0014: stloc.s shift33a 

我理解发生了什么 (在MSDN中描述); 当编译代码时,在移位32位值时只使用低5位…我很好奇为什么会发生这种情况。

shift33a出来的方式也让我觉得Reflector有些不太正确,因为他们的IL的c#表示会编译成不同的东西)

问题:

  • 为什么只使用“要转移的值”的低5位?
  • 如果“移位超过31位没有意义”,为什么没有警告?
  • 这是一个向后兼容的事情(即这是程序员“期望”发生的事情)吗?
  • 我是否正确,底层IL可以进行超过31位的移位(如L_0010: ldc.i4.s 0x21 ),但编译器正在修改值?

它基本上归结为x86处理算术移位操作码的方式:它只使用移位计数的底部5位。 例如,请参阅80386编程指南 。 在C / C ++中,技术上未定义的行为是将位移超过31位(对于32位整数),采用C语言“你不为你不需要的东西买单”。 根据C99标准第6.5.7节第3段:

对每个操作数执行整数提升。 结果的类型是提升的左操作数的类型。 如果右操作数的值为负或大于或等于提升的左操作数的宽度,则行为未定义。

这允许编译器在x86上省略单个移位指令以进行移位。 在x86上的一条指令中无法进行64位移位。 他们使用SHLD / SHRD指令以及一些额外的逻辑。 在x86_64上,可以在一条指令中完成64位移位。

例如,gcc 3.4.4为64位左移发出以下程序集任意数量(使用-O3 -fomit-frame-pointer编译):

 uint64_t lshift(uint64_t x, int r) { return x << r; } _lshift: movl 12(%esp), %ecx movl 4(%esp), %eax movl 8(%esp), %edx shldl %cl,%eax, %edx sall %cl, %eax testb $32, %cl je L5 movl %eax, %edx xorl %eax, %eax L5: ret 

现在,我对C#不太熟悉,但我猜它有类似的理念 - 设计语言以尽可能高效地实现它。 通过指定移位操作仅使用移位计数的底部5/6位,它允许JIT编译器尽可能最佳地编译移位。 32位移位以及64位系统上的64位移位可以将JIT编译为单个操作码。

如果将C#移植到对其本机移位操作码具有不同行为的平台,那么这实际上会产生额外的性能损失 - JIT编译器必须确保标准得到尊重,因此它必须添加额外的逻辑确保仅使用移位计数的底部5/6位。

Unit32以32位溢出,这是在规范中定义的。 你有什么期待?

CLR没有使用溢出检测运算符(1)定义左移。 如果您需要这种设施,您需要自己检查。

(1)C#编译器可能会把它强制转换,但我不确定。

我在C(gcc,linux)中编写了这个简单的测试,并得到了类似的结果。 有趣的是,过度移位的常数定义变为零而不是环绕。 它确实给出了关于那些的警告,所以至少有人承认这是一个“不正确”的事情。

 #include  unsigned int is0 = 1 << 31; unsigned int is1 = 1 << 32; unsigned int is2 = 1 << 33; int main() { unsigned int loopy = 0; int x = 0; printf("0x%08X\n", is0); printf("0x%08X\n", is1); printf("0x%08X\n", is2); for (x = 0; x < 35; ++x) { loopy = 1 << x; printf("%02d 0x%08X\n", x,loopy); } return 0; } 

结果如下:

 0x80000000 0x00000000 0x00000000 00 0x00000001 01 0x00000002 02 0x00000004 03 0x00000008 04 0x00000010 05 0x00000020 06 0x00000040 07 0x00000080 08 0x00000100 09 0x00000200 10 0x00000400 11 0x00000800 12 0x00001000 13 0x00002000 14 0x00004000 15 0x00008000 16 0x00010000 17 0x00020000 18 0x00040000 19 0x00080000 20 0x00100000 21 0x00200000 22 0x00400000 23 0x00800000 24 0x01000000 25 0x02000000 26 0x04000000 27 0x08000000 28 0x10000000 29 0x20000000 30 0x40000000 31 0x80000000 32 0x00000001 33 0x00000002 34 0x00000004