为什么生成AND指令?
对于这样的代码:
int res = 0; for (int i = 0; i < 32; i++) { res += 1 << i; }
此代码生成(发布模式,没有附加debugging器,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已经被0x1F掩码了。 所以在我看来,这些AND完全没有用处。 他们为什么生成? 他们有我失踪的目的吗?
and
已经出现在C#编译器发出的CIL代码中:
IL_0009: ldc.i4.s 31 IL_000b: and IL_000c: shl
CIL shl
指令的规范说:
如果shiftAmount大于或等于值的大小,则返回值未指定。
然而,C#规范定义了32位的移位,以移位计数模式32:
当x的types是
int
或uint,
移位计数由count的低位5位给出。 换句话说,移位计数从count & 0x1F
计算。
在这种情况下,C#编译器无法真正做得比发出显式操作好得多。 最好你能希望的是,JITter会注意到这一点,并优化掉冗余,但这需要时间,而JIT的速度是相当重要的。 所以考虑一下基于JIT的系统的价格。
我想,真正的问题就是为什么当C#和x86都指定了截断行为时,CIL以这种方式指定了shl
指令。 我不知道,但是我推测CIL规范避免指定一些可能会导致某些指令集上的代价太高的行为是很重要的。 与此同时,对C#来说尽可能less的未定义的行为是很重要的,因为人们总是最终使用这些未定义的行为,直到下一版本的编译器/框架/操作系统/不pipe怎样改变它们,破坏代码。
x64内核已经将5位掩码应用于移位量。 从“英特尔处理器”手册中,卷2B第4-362页:
目标操作数可以是寄存器或内存位置。 计数操作数可以是立即数或CL寄存器。 计数被屏蔽到5位 (如果使用64位模式并使用REG.W,则为6位)。 提供了一个特殊的操作码编码,计数为1。
所以这是没有必要的机器代码。 不幸的是,C#编译器无法对处理器的行为作出任何假设,并且必须应用C#语言规则。 并生成其行为在CLI规范中指定的IL。 Ecma-335,Partion III,3.58章对SHL操作码进行了说明:
shl指令将值(int32,int64或本地int)左移由shiftAmount指定的位数。 shiftAmounttypes为int32或本地int。 如果shiftAmount大于或等于值的宽度,则返回值是未指定的 。
未指定是这里的蹭。 在未指定的实现细节之上插入指定的行为会产生不必要的代码。 从技术上讲,抖动可以优化操作码。 虽然这很棘手,但它不知道语言规则。 任何指定没有屏蔽的语言将很难生成适当的IL。 您可以发布到connect.microsoft.com以获取有关此问题的抖动团队的观点。
C#编译器必须在生成中间(与机器无关的)代码时插入这些AND指令,因为C#左移运算符只需要使用5个最低有效位。
在生成x86代码时,优化编译器可能会丢弃这些不需要的指令。 但是,显然,它跳过了这个优化(可能,因为它不能花太多时间在分析上)。