C# 方法内联

起因

最近有时间会走马观花式的看.Net Runtime源码,经常会看到会使用特性,标记方法函数如果可以内联就内联.在项目中还没有使用过,是因为原先项目使用的.Net Framework版本有些低.

方法内联这个技术并不新,也不潮,在C/C++是通过inline关键字进行的,或者使用宏定义,没错,通过inline标记具体是不是进行内联,是由编译器决定的,如果方法内联,就会将方法内的代码移到调用的地方.这样就没有调用方法的开销,从而达到提高性能的目的.

方法内联带来也不全是好处,可能会造成可执行程序的体积变大.当然这一点在.Net是没有的,是因为在.Net中决定方法是否进行内联,是有JIT编译器决定的,虽然不会造成.Net程序体积变大,第一次调用的时候,JIT编译器会将标记内联方法的转为汇编代码,所以还是会影响性能的.

在.Net Core 3.0及之后的版本(.Net Core 3.1/.Net 5/.Net 6)默认开启分层编译,分层编译分为(0层和1层),0层只对方法生成机器码,不对代码进行太多的优化,1层编译会对调用达到条件(100毫秒内,调用30次方法)的方法进行优化,优化后重新生成机器码.

那些方法不是被JIT内联优化:
  1. 虚方法
  2. 接口中的方法,不确定调用那个类型的方法
  3. 方法生成的IL代码大于32字节.

关于C#中的特性MethodImpl, 相关选项: MethodImplOptions 枚举
MethodImpl 可用的选项,由我们决定使用那个,是否要方法内联,是否禁止方法内联
上图红色标记的为常用/常见的.

测试代码

using System.Runtime.CompilerServices;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Configs;
using BenchmarkDotNet.Exporters;
using BenchmarkDotNet.Exporters.Csv;

namespace dotnet_perf
{
    [RPlotExporter]
    [MemoryDiagnoser]
    [DisassemblyDiagnoser(printSource: true)]
    [Config(typeof(Config))]
    public class MethodInlineTest
    {
        [Params(10000)]
        public int Count { get; set; }

        public Random Random = new Random();



        [Benchmark]
        public void AggressiveInlining()
        {
            for (int i = 0; i < Count; i++)
            {
                int a = Random.Next(1, 100);
                int b = Random.Next(1, 100);
                int c = AggressiveInliningTest(a, b);
            }
        }

        [Benchmark]
        public void AggressiveOptimization()
        {
            for (int i = 0; i < Count; i++)
            {
                int a = Random.Next(1, 100);
                int b = Random.Next(1, 100);
                int c = AggressiveOptimizationTest(a, b);
            }
        }

        [Benchmark]
        public void NoInlining()
        {
            for (int i = 0; i < Count; i++)
            {
                int a = Random.Next(1, 100);
                int b = Random.Next(1, 100);
                int c = NoInliningnTest(a, b);
            }
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public int AggressiveInliningTest(int a, int b)  //求数的大小
        {
            return a > b ? a : b;
        }

        [MethodImpl(MethodImplOptions.AggressiveOptimization)]
        public int AggressiveOptimizationTest(int a, int b)  //求数的大小
        {
            return a > b ? a : b;
        }

        [MethodImpl(MethodImplOptions.NoInlining)]
        public int NoInliningnTest(int a, int b)  //求数的大小
        {
            return a > b ? a : b;
        }


        private class Config : ManualConfig
        {
            public Config()
            {
                //这里配置BenchmarkDotNet 生成图表,其实是生成R的脚本
                AddExporter(CsvMeasurementsExporter.Default);
                AddExporter(RPlotExporter.Default);  

            }
        }
    }
}

看一下生成的汇编代码(因为生成的汇编代码较长,这里只展示内联和没有内联的差异):

先看没有内联的代码:

; dotnet_perf.MethodInlineTest.NoInlining()
       push      rdi
       push      rsi
       push      rbx
       sub       rsp,20
       mov       rsi,rcx
;             for (int i = 0; i < Count; i++)
;                  ^^^^^^^^^
       xor       edi,edi
       cmp       dword ptr [rsi+10],0
       jle       short M00_L01
;                 int a = Random.Next(1, 100);
;                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
M00_L00:
       mov       rcx,[rsi+8]
       mov       edx,1
       mov       r8d,64
       mov       rax,[rcx]
       mov       rax,[rax+40]
       call      qword ptr [rax+30]
       mov       ebx,eax
;                 int b = Random.Next(1, 100);
;                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
       mov       rcx,[rsi+8]
       mov       edx,1
       mov       r8d,64
       mov       rax,[rcx]
       mov       rax,[rax+40]
       call      qword ptr [rax+30]
       mov       r8d,eax
;                 int c = NoInliningnTest(a, b);
;                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
       mov       rcx,rsi
       mov       edx,ebx
;调用NoInliningnTest方法
       call      dotnet_perf.MethodInlineTest.NoInliningnTest(Int32, Int32)
       inc       edi
       cmp       edi,[rsi+10]
       jl        short M00_L00
M00_L01:
       add       rsp,20
       pop       rbx
       pop       rsi
       pop       rdi
       ret
; Total bytes of code 98
; NoInliningnTest生成汇编代码
; dotnet_perf.MethodInlineTest.NoInliningnTest(Int32, Int32)
;             return a > b ? a : b;
;             ^^^^^^^^^^^^^^^^^^^^^
       cmp       edx,r8d
       jg        short M01_L00
       mov       eax,r8d
       ret
M01_L00:
       mov       eax,edx
       ret
; Total bytes of code 12

在内联的时候生成汇编代码:

; 在调用使用AggressiveInlining标记后,我们没有有方法调用的指令
; dotnet_perf.MethodInlineTest.AggressiveInlining()
       push      rdi
       push      rsi
       sub       rsp,28
       mov       rsi,rcx
;             for (int i = 0; i < Count; i++)
;                  ^^^^^^^^^
       xor       edi,edi
       cmp       dword ptr [rsi+10],0
       jle       short M00_L01
;                 int a = Random.Next(1, 100);
;                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
M00_L00:
       mov       rcx,[rsi+8]
       mov       edx,1
       mov       r8d,64
       mov       rax,[rcx]
       mov       rax,[rax+40]
       call      qword ptr [rax+30]
;                 int b = Random.Next(1, 100);
;                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
       mov       rcx,[rsi+8]
       mov       edx,1
       mov       r8d,64
       mov       rax,[rcx]
       mov       rax,[rax+40]
       call      qword ptr [rax+30]
       inc       edi
       cmp       edi,[rsi+10]
       jl        short M00_L00
M00_L01:
       add       rsp,28
       pop       rsi
       pop       rdi
       ret
; Total bytes of code 81


看BenchmarkDotNet性能测试的结果:

通过BenchmarkDotNet测试方法内联是否带来性能的提升

这里只是测试了.Net Core 3.1/.Net 5/.Net 6这三个版本,

  1. NoInlining在不使用内联优化,在.Net Core 3.1和.Net 6对比性能相差了35%.说明.Net 6性能是可以让人相信的.
  2. AggressiveOptimization在.Net Core 3.1和.Net 6对比性能相差了34%,.Net 5在这里没有存在感.
  3. AggressiveInlining在内联优化下.Net Core 3.1和.Net 6对比性能相差了35%,.Net 5比.Net 3.1还不如.
  4. 在.Net 6中,使用内联比不内联,性能相差了20%左右.
我们看一下生成的图表,这个更直观一些:

方法内联和没有内联在.Net 不同版本性能对比趋势图
秋风 2021-08-16