【问题标题】:Why use only the lower five bits of the shift operand when shifting a 32-bit value? (e.g. (UInt32)1 << 33 == 2)为什么在移位 32 位值时只使用移位操作数的低五位? (例如(UInt32)1 << 33 == 2)
【发布时间】:2009-03-13 21:37:04
【问题描述】:

考虑以下代码:

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

【问题讨论】:

  • 甚至规范都没有提供关于“为什么”的见解。您可以尝试询问 Eric Lippert 等 C# 编译器专家。
  • @Johannes:是的,我只是想知道是否有人有一个用例或某些东西,为什么这样做是有意义的:-/
  • 你永远不应该相信 Reflector。我花了好几个小时来寻找错误,因为我相信输出。 IL 通常是最可靠的,但我也遇到过一些不太顺利的情况。改用 ILDASM。
  • @leppie:是的,这就是我发布 IL 的原因:-]

标签: c# bit-shift


【解决方案1】:

它基本上归结为 x86 处理算术移位操作码的方式:它只使用移位计数的低 5 位。例如,请参阅80386 programming guide。在 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 位。

【讨论】:

  • 那么shift33a的解释是什么?这是向后兼容的事情吗?
  • 我怀疑 C# 作为一种 CLR 语言,是否应该非常关注 x68 的行为方式。当然,它是唯一得到官方支持的平台,但虚拟机仍然不需要与现有处理器有太多兼容性。
  • 我在 ISO C99 (draft) 中没有看到关于 bitshift >31 未定义的任何内容,我也不明白为什么会这样。 C 与字数无关。
  • IL 似乎能够进行超过 31 位的移位(shift33a 就是这种情况,它变成了L_0010: ldc.i4.s 0x21,结果为 0)
  • 而如果你更进一步,问“为什么硬件会这样?”,这里似乎得到了答案:stackoverflow.com/questions/10932578/…——本质上,硬件只是为了方便抓住相关位。
【解决方案2】:

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

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

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

【讨论】:

  • 嗯,直观地说,我希望左移 1 32 位会得到 0(因为溢出),而不是 1。
  • 未定义,CLR 可以为所欲为。我不制定规则... :)
  • 是的,看download.microsoft.com/download/D/C/1/…:如果“Shift-By”操作数大于“To-Be-Shifted”操作数的宽度,则结果未指定。”
  • 我一直在看你的问题,但我无法理解你所说的 5 位是什么意思,你从哪里得到的?
  • 我的意思是,在确定将值移位多少位时,实际上只使用了移位量的低 5 位。所以 32 位值的 33 移位实际上是 1 的移位(因为 33 的低五位 == 00001)
【解决方案3】:

我用 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

【讨论】:

  • 这段代码有未定义的含义。移位 >= 32 位是未定义的。因此,您的程序无法可靠地显示任何内容。
猜你喜欢
  • 2021-02-09
  • 2013-09-18
  • 1970-01-01
  • 2011-02-08
  • 2015-04-02
  • 1970-01-01
  • 1970-01-01
  • 2022-11-17
  • 1970-01-01
相关资源
最近更新 更多