1.前言
.NET8通過各種騷操,把性能提升到了前所未有的高度。超越以往任何版本,也涵蓋了后續版本,比如.NET9或許可能沒有如此大的性能優化了。本篇來看下它其中的一個優化:類型轉換的優化效果。
2.示例
通過類型檢查的優化,優化掉某些情況下類型轉換的時候JIT類型檢查的函數。下面的代碼是類型檢查的典型應用。
[HideColumns("Error", "StdDev", "Median", "RatIOSD")]
[DisassemblyDiagnoser(maxDepth: 0)]
public class Tests
{
private readonly string[] _strings = new string[1];
[Benchmark]
public string Get1() => _strings[0];
[Benchmark]
public string Get2() => Volatile.Read(ref _strings[0]);
}
public partial class Program
{
static void MAIn(string[] args)
{
BenchmarkSwitcher.FromAssembly(typeof(Tests).Assembly).Run(args);
}
}
我們看到_strings是個私有數組,Get1函數中獲取_strings數組的第一個值。所以它是直接用ldelem.ref IL執行即可
ldelem.ref
但是Get2里面對數組元素進行了引用,所以Roslyn的指令是:
ldelema [System.Runtime]System.String
如果ref類型的變量,被賦值為不同于這個變量的類型則會違反類型安全性。通常情況下ldelema需要進行類型檢查,也就是用JIT輔助函數CORINFO_HELP_LDELEMA_REF來進行檢查,以確保不會違反類型安全性。
這個安全性的檢查會極大損耗性能,.NET8的JIT進行了一個優化,思路是如果是sealed關鍵字標記的類型,就不會進行安全性檢查,這樣就會提高性能。為什么sealed不會呢?
這其實是利用了它的一個特性,就是不會被繼承。不會被繼承,就不會被子類的類型所困擾,只有string一個類型,自然不會用以進行類型檢查了。
這是第一點優化,下面看下。
3.第一階優化
優化了類型安全檢查,縮短了編譯時間,提高了性能。來看下.Net7和.NET8的生成Get2函數的的不同點
.Net7:
Tests.Get2()
sub rsp,28
mov rcx,[rcx+8]
xor edx,edx
mov r8,offset MT_System.String
call CORINFO_HELP_LDELEMA_REF
mov rax,[rax]
add rsp,28
ret
; Total bytes of code 33
.Net7它這里有一個CORINFO_HELP_LDELEMA_REF進行安全性檢查。
.Net8:
; Tests.Get2()
sub rsp,28
mov rax,[rcx+8]
cmp dword ptr [rax+8],0
jbe short M00_L00
mov rax,[rax+10]
add rsp,28
ret
M00_L00:
call CORINFO_HELP_RNGCHKFAIL
int 3
; Total bytes of code 29
.Net8里它沒有了CORINFO_HELP_LDELEMA_REF
因為string類型是sealed,它的原型如下:
public sealed class String : IEnumerable<char>, IEnumerable, ICloneable, IComparable, IComparable<String?>, IConvertible, IEquatable<String?>
{
//這里代碼省略
}
JIT會判斷類型是否是sealed標志,如果是則不進行安全性檢查優化。
雖然.Net8去掉了CORINFO_HELP_LDELEMA_REF,
但是多了范圍的檢查CORINFO_HELP_RNGCHKFAIL,那它這個性能如何呢?
我們來測試下:
dotnet run -c Release -f net7.0 --filter "*" --runtimes net7.0 net8.0
結果是:
Method |
Runtime |
Mean |
Ratio |
Code Size |
Get2 |
.NET 7.0 |
1.0537 ns |
1.00 |
33 B |
Get2 |
.NET 8.0 |
0.2423 ns |
0.23 |
29 B |
我們看到同樣代碼,.Net8里面比.Net7的性能提升了5倍之多。
4.第二階優化
承接上面,上面sealed去掉了類型檢查。
然后在類型轉換的時候,一般的類型轉換JIT使用的是CastHelpers.ChkCastAny來進行。
但是.Net8里面內聯了一個方法
用以縮短CastHelpers.ChkCastAny的編譯時間,提高編譯的時間和程序的性能。
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
using System.Runtime.CompilerServices;
BenchmarkSwitcher.FromAssembly(typeof(Tests).Assembly).Run(args);
[HideColumns("Error", "StdDev", "Median", "RatioSD")]
public class Tests
{
private readonly object _o = "hello";
[Benchmark]
public string GetString() => Cast<string>(_o);
[MethodImpl(MethodImplOptions.NoInlining)]
public T Cast<T>(object o) => (T)o;
}
同樣的
dotnet run -c Release -f net7.0 --filter "*" --runtimes net7.0 net8.0
結果如下:
Method |
Runtime |
Mean |
Ratio |
GetString |
.NET 7.0 |
3.018 ns |
1.00 |
GetString |
.NET 8.0 |
1.198 ns |
0.40 |
.Net8是三倍于.Net7的運行速度。去掉類型檢查+類型轉換的內聯,大幅度的提升效率,可見.Net8的性能優化確實不容小覷。
參考如下:
https://devblogs.microsoft.com/dotnet/performance-improvements-in-net-8/
最后推薦下個人的CLR/JIT交流圈,里面有多篇個人編寫的高質量的原創欄目和文章。學習心得,項目經驗等。帶你進入.Net核心技術階層,脫離curd工程師范疇。