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