请考虑以下代码:
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
Run Code Online (Sandbox Code Playgroud)
它不会生成警告(关于使用大于32的班次),因此它必须是预期的行为.
实际上被放到生成的程序集中的代码(或者至少是Reflector对代码的解释)是
uint val = 1;
uint shift31 = val << 0x1f;
uint shift32 = val;
uint shift33 = val << 1;
uint shift33a = val << 0x21;
Run Code Online (Sandbox Code Playgroud)
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
Run Code Online (Sandbox Code Playgroud)
我理解发生了什么(在MSDN中描述); 当编译代码时,在移位32位值时只使用低5位......我很好奇为什么会发生这种情况.
(shift33a出来的方式也让我觉得Reflector有些不太正确,因为他们的IL的c#表示会编译成不同的东西)
问题:
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
Run Code Online (Sandbox Code Playgroud)
现在,我对C#不太熟悉,但我猜它有类似的理念 - 设计语言以尽可能高效地实现它.通过指定移位操作仅使用移位计数的底部5/6位,它允许JIT编译器尽可能最佳地编译移位.32位移位以及64位系统上的64位移位可以将JIT编译为单个操作码.
如果将C#移植到对其本机移位操作码具有不同行为的平台,那么这实际上会产生额外的性能损失 - JIT编译器必须确保标准得到尊重,因此它必须添加额外的逻辑确保仅使用移位计数的底部5/6位.
| 归档时间: |
|
| 查看次数: |
1995 次 |
| 最近记录: |