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内联优化:
- 虚方法
- 接口中的方法,不确定调用那个类型的方法
- 方法生成的IL代码大于32字节.

上图红色标记的为常用/常见的.
测试代码
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性能测试的结果:
这里只是测试了.Net Core 3.1/.Net 5/.Net 6这三个版本,
- NoInlining在不使用内联优化,在.Net Core 3.1和.Net 6对比性能相差了35%.说明.Net 6性能是可以让人相信的.
- AggressiveOptimization在.Net Core 3.1和.Net 6对比性能相差了34%,.Net 5在这里没有存在感.
- AggressiveInlining在内联优化下.Net Core 3.1和.Net 6对比性能相差了35%,.Net 5比.Net 3.1还不如.
- 在.Net 6中,使用内联比不内联,性能相差了20%左右.
秋风
2021-08-16