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

问题描述

我遇到了 .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,int curIndex)
        {
            RotateLeft(ref hash,curIndex);
            hash ^= data[curIndex];
            if (curIndex < SmallestMatch)
            {
                ulong value = data[curIndex - SmallestMatch];
                RotateLeft(ref value,Offset);
                hash ^= value;
            }
        }
    }
}

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

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