14

考虑以下代码:

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 的移位),因此它必须是预期的行为。

实际输出到生成程序集的代码(或至少 Reflector 对代码的解释)是

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

IL(再次,使用反射器)是

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),但编译器正在修整这些值?
4

3 回答 3

9

它基本上归结为 x86 处理算术移位操作码的方式:它只使用移位计数的低 5 位。例如,参见80386 编程指南。在 C/C++ 中,将位移位超过 31 位(对于 32 位整数)在技术上是未定义的行为,这符合 C 哲学“你不需要为不需要的东西付费”。从 C99 标准的第 6.5.7 节第 3 段:

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

这允许编译器在 x86 上为移位省略单个移位指令。64 位移位不能在 x86 上的一条指令中完成。他们使用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 位。

于 2009-03-13T21:44:41.137 回答
3

Unit32 在规范中定义的 32 位溢出。你在期待什么?

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

(1) C# 编译器可能会将其转换为 long,但我不确定。

于 2009-03-13T22:32:21.053 回答
1

我用 C (gcc, linux) 编写了这个简单的测试,并得到了类似的结果。有趣的是,过度移位的常量定义变成了零,而不是环绕。它确实对这些发出了警告,所以至少有人认识到这是一件“不正确的”事情。

#include <stdio.h>

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
于 2009-03-13T22:04:13.947 回答