请考虑以下代码:
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(再次,使用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
),但编译器正在修改值?
Adam Rosenfi.. 9
它基本上归结为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位.
它基本上归结为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位.