为什么生成AND指令?

时间:2012-03-28 10:31:41

标签: c# .net assembly

对于这样的代码:

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完全没用。他们为什么生成?他们有一些我失踪的目的吗?

3 个答案:

答案 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 的类型为intuint,时,移位计数由计数的低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代码时,优化编译器可能会删除这些不需要的指令。但是,显然,它会跳过这种优化(可能是因为它无法在分析上花费太多时间)。