为什么 .NET CLR 没有正确地内联它?

Why does the .NET CLR not inline this properly?

我 运行 发现 .NET JIT 编译器的内联行为不太理想。下面的代码去掉了它的上下文,但它演示了问题:

using System.Runtime.CompilerServices;

namespace HashTest
{
    public class Hasher
    {
        private const int hashSize = sizeof(ulong) * 8;
        public int SmallestMatch;
        public int Offset;
        public void Hash_Inline(ref ulong hash, byte[] data, int curIndex)
        {
            hash = (hash << 1) | (hash >> (hashSize - 1));
            hash ^= data[curIndex];
            if (curIndex < SmallestMatch)
            {
                ulong value = data[curIndex - SmallestMatch];
                value = (value << Offset) | (value >> (hashSize - Offset));
                hash ^= value;
            }
        }
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        private void RotateLeft(ref ulong hash, int by)
        {
            hash = (hash << by) | (hash >> (hashSize - by));
        }
        public void Hash_FunctionCall(ref ulong hash, byte[] data, int curIndex)
        {
            RotateLeft(ref hash, curIndex);
            hash ^= data[curIndex];
            if (curIndex < SmallestMatch)
            {
                ulong value = data[curIndex - SmallestMatch];
                RotateLeft(ref value, Offset);
                hash ^= value;
            }
        }
    }
}

这是它在运行时使用发布版本生成的汇编代码(只是函数的第一行,直到每个函数第二行的异或):

net472, inline code:

000007FE8E8E0A20  sub         rsp,28h  
000007FE8E8E0A24  rol         qword ptr [rdx],1  

net472, aggressive inlining:

000007FE8E8E09B0  sub         rsp,28h  
000007FE8E8E09B4  mov         qword ptr [rsp+30h],rcx  
000007FE8E8E09B9  mov         ecx,r9d  
000007FE8E8E09BC  rol         qword ptr [rdx],cl

net5.0, inline code:

000007FE67E56040  push        rbp  
000007FE67E56041  sub         rsp,30h  
000007FE67E56045  lea         rbp,[rsp+30h]  
000007FE67E5604A  xor         eax,eax  
000007FE67E5604C  mov         qword ptr [rbp-8],rax  
000007FE67E56050  mov         qword ptr [rbp+10h],rcx  
000007FE67E56054  mov         qword ptr [rbp+18h],rdx  
000007FE67E56058  mov         qword ptr [rbp+20h],r8  
000007FE67E5605C  mov         dword ptr [rbp+28h],r9d  
000007FE67E56060  mov         rcx,qword ptr [rbp+18h]  
000007FE67E56064  rol         qword ptr [rcx],1  

net5.0, agressive inlining:

000007FE67E55B30  push        rbp  
000007FE67E55B31  sub         rsp,30h  
000007FE67E55B35  lea         rbp,[rsp+30h]  
000007FE67E55B3A  xor         eax,eax  
000007FE67E55B3C  mov         qword ptr [rbp-8],rax  
000007FE67E55B40  mov         qword ptr [rbp+10h],rcx  
000007FE67E55B44  mov         qword ptr [rbp+18h],rdx  
000007FE67E55B48  mov         qword ptr [rbp+20h],r8  
000007FE67E55B4C  mov         dword ptr [rbp+28h],r9d  
000007FE67E55B50  mov         rcx,qword ptr [rbp+10h]  
000007FE67E55B54  mov         rdx,qword ptr [rbp+18h]  
000007FE67E55B58  mov         r8d,dword ptr [rbp+28h]  
000007FE67E55B5C  call        CLRStub[MethodDescPrestub]@7fe67e55558 (07FE67E55558h)  


000007FE67E56010  push        rbp  
000007FE67E56011  mov         rbp,rsp  
000007FE67E56014  mov         qword ptr [rbp+10h],rcx  
000007FE67E56018  mov         qword ptr [rbp+18h],rdx  
000007FE67E5601C  mov         dword ptr [rbp+20h],r8d  
000007FE67E56020  mov         ecx,dword ptr [rbp+20h]  
000007FE67E56023  mov         rax,qword ptr [rbp+18h]  
000007FE67E56027  rol         qword ptr [rax],cl  
000007FE67E5602A  pop         rbp  
000007FE67E5602B  ret  

为什么它们不都生成相同的代码,即第一个示例中的 2 指令版本?有没有办法将“旋转”放在函数中而不是必须内联它?

编辑:我在发布的 RotateLeft 代码中发现了一个错误,它大大改进了生成程序集,但仍然存在问题。

函数Hash_InlineHash_FunctionCall不等价:

  • Hash_Inline 中的第一个语句旋转 1,但在 Hash_FunctionCall 中它旋转 curIndex
  • 对于 RotateLeft 你可能的意思是:
private void RotateLeft(ref ulong hash, int by)
{
    hash = (hash << by) | (hash >> (hashSize - by));
}

(注意:已编辑此问题以解决此问题)

如果修复这两个问题,JIT 编译器会为 .NET 5(但不是 .NET Framework)上的两个函数生成相同的本机代码:请参阅反汇编 here

此外,如果您使用的是 .NET Core 3.0 或更高版本并且想要反汇编完全优化的代码,则需要调用该函数的次数足够多(以触发 tier 1 compilation)在进行反汇编之前,或使用 MethodImplOptions.AggressiveOptimization.