对于这样的代码:
int res = 0;
for (int i = 0; i < 32; i++)
{
res += 1 << i;
}
生成此代码(发布模式,未附加调试器,64位):
xor edx,edx
mov r8d,1
_loop:
lea ecx,[r8-1]
and ecx,1Fh ; why?
mov eax,1
shl eax,cl
add edx,eax
mov ecx,r8d
and ecx,1Fh ; why?
mov eax,1
shl eax,cl
add edx,eax
lea ecx,[r8+1]
and ecx,1Fh ; why?
mov eax,1
shl eax,cl
add edx,eax
lea ecx,[r8+2]
and ecx,1Fh ; why?
mov eax,1
shl eax,cl
add edx,eax
add r8d,4
cmp r8d,21h
jl _loop
现在我可以看到大多数指令的重点,但是AND指令是什么?无论如何,ecx在此代码中永远不会超过0x1F,但我原谅它没有注意到(并且还没有注意到结果是常量),它不是一个提前编译器毕竟,这可以花费很多时间在分析上。但更重要的是,具有32位操作数的SHL已经将0x掩码为0x1F。所以在我看来,这些AND完全没用。他们为什么生成?他们有一些我失踪的目的吗?
答案 0 :(得分:27)
and
已存在于C#编译器发出的CIL代码中:
IL_0009: ldc.i4.s 31
IL_000b: and
IL_000c: shl
CIL shl
指令的规范说:
如果 shiftAmount 大于或等于 value 的大小,则未指定返回值。
然而,C#规范定义了32位移位以采用移位计数模式32:
当 x 的类型为
int
或uint,
时,移位计数由计数的低5位给出。换句话说,移位计数是从count & 0x1F
计算的。
在这种情况下,C#编译器实际上并不比发出明确的and
操作好得多。你可以期待的最好是JITter会注意到这一点并优化掉冗余的and
,但这需要时间,而JIT的速度非常重要。因此,请考虑为基于JIT的系统支付的价格。
我想,真正的问题是,当C#和x86都指定截断行为时,CIL以这种方式指定shl
指令的原因。我不知道,但我推测,对于CIL规范来说,避免在某些指令集上指定可能JIT到某些昂贵的行为是很重要的。同时,对于C#来说,拥有尽可能少的未定义行为非常重要,因为人们总是最终使用这种未定义的行为,直到下一版本的编译器/框架/ OS /无论如何改变它们,都会破坏代码。
答案 1 :(得分:10)
x64内核已经将5位掩码应用于移位量。从英特尔处理器手册,第2B卷第4-362页:
目标操作数可以是寄存器或内存位置。计数操作数可以是立即值或CL寄存器。 计数被屏蔽为5位(如果在64位模式下使用REG.W则为6位)。计数为1的特殊操作码编码。
所以这是不必要的机器代码。不幸的是,C#编译器不能对处理器的行为做任何假设,必须应用C#语言规则。并生成IL,其行为在CLI规范中指定。 Ecma-335,Partion III,第3.58章说明了SHL操作码:
shl指令将shiftAmount指定的位数左移(int32,int64或native int)。 shiftAmount的类型为int32或native int。 如果shiftAmount大于或等于值的宽度,则返回值未指定。
未指定就在这里。在未指定的实现细节之上抽取指定的行为会产生不必要的代码。从技术上讲,抖动可以优化操作码。虽然这很棘手,但它并不知道语言规则。任何指定不屏蔽的语言都会很难生成适当的IL。您可以发布到connect.microsoft.com以获得抖动团队对此事的看法。
答案 2 :(得分:5)
C#编译器必须在生成中间(机器无关)代码时插入这些AND指令,因为C#左移运算符只需要使用5个最低有效位。
在生成x86代码时,优化编译器可能会删除这些不需要的指令。但是,显然,它会跳过这种优化(可能是因为它无法在分析上花费太多时间)。