.NET8對於效能的優化是方方面面的,所以AOT預編譯機器碼也是不例外的。本篇來看下對於AOT的優化。原文:.NET8極致效能優化AOT
首先明確一個概念,.NET裡面的AOT它是原生的。什麼意思呢?也就是說通過ILC編譯器(AOT編譯器,參考:.Net 7 新編譯器 ILC 簡析)編譯出來的程式碼是各個平臺上可以直接執行的二進位制程式碼。比如MacOS的二進位制,Linux二進位制等等。所以稱之為原生。
C#原始碼被ILC編譯之後,生成了一個完全原生態程式碼的可執行檔案。在執行的時候不需要JIT來編譯任何東西,因為JIT已經在ILC裡面被充分利用過了。實際上AOT裡面也沒有包含JIT。那麼它如何優化呢?只能是在ILC裡面呼叫JIT的時候了。所以它這個優化依然依靠JIT。.NET8裡面優化AOT的一個典型的例子,就是ASP.NET應用程式在使用AOT的時候表現不錯,同時也降低了總成本。
在.NET8裡面優化AOT的一個重要的目標就是減少AOT可執行檔案的大小,關於這點的效果。我們現在就可以看到
下面建立一個控制檯應用程式
dotnet new console -o nativeaotexample -f net7.0
由於上面是通過.NET7.0建立的,我們把這個控制檯的csproj更改下
<TargetFramework>net7.0</TargetFramework>
改為
<TargetFrameworks>net7.0;net8.0</TargetFrameworks>
可以輕鬆的構建.NET7.0或者.NET8.0的程式
繼續
把<PropertyGroup>...</PropertyGroup>項中新增如下
<PublishAot>true</PublishAot>編譯成AOT檔案
下面我們就可以通過dotnet publish釋出它了,linux如下:
dotnet publish -f net7.0 -r linux-x64 -c Release
現在它生成了一個.NET7.0版本的獨立可執行檔案,可通過 ls/dir 輸出目錄以檢視生成的二進位制大小
12820K /home/stoub/nativeaotexample/bin/Release/net7.0/linux-x64/publish/nativeaotexample
這個大約是13M左右,我們再來看下.NET8.0
dotnet publish -f net8.0 -r linux-x64 -c Release
生成的可執行檔案大小如下:
1536K /home/stoub/nativeaotexample/bin/Release/net8.0/linux-x64/publish/nativeaotexample
1.5M的大小,這個優化的力度不可不大。優化了將近10倍的體積。這就是.NET8.0的優化魔力。
但是優化的情況遠不止如此,比如說我們可以設定csproj使AOT的體積更小
csproj新增如下size表示要生成的AOT大小
<OptimizationPreference>Size</OptimizationPreference>
如果我們不需要全球化程式碼和資料,需要特定的程式碼和資料,並且使用不變模式,可以csproj新增如下選項
<InvariantGlobalization>true</InvariantGlobalization>
如果你不想在AOT異常的時候丟擲堆疊,那麼你也可以在csproj裡面新增如下
<StackTraceSupport>false</StackTraceSupport>
重新通過dotnet publish net8.0釋出了之後,它的體積還可以繼續減小
1248K /home/stoub/nativeaotexample/bin/Release/net8.0/linux-x64/publish/nativeaotexample
再次縮小了0.3M大小。
然而,你以為到此優化就為止了嗎?並沒有,.NET8不僅對AOT編譯器內部進行了改進,而且還對單個庫也進行了效能優化和改進。比如HttpClient。
當然除了體積的優化之外,還有其它的優化,比如避免了在讀取靜態欄位時的輔助呼叫,再比如BenchmarkDotNet 也是支援AOT化的,也就是效能測試上面的支援。我們可以只使用 --runtimes nativeaot7.0 nativeaot8.0,而不使用 --runtimes net7.0 net8.0,如下程式碼
// dotnet run -c Release -f net7.0 --filter "*" --runtimes nativeaot7.0 nativeaot8.0
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
BenchmarkSwitcher.FromAssembly(typeof(Tests).Assembly).Run(args);
[HideColumns("Error", "StdDev", "Median", "RatioSD")]
public class Tests
{
private static readonly int s_configValue = 42;
[Benchmark]
public int GetConfigValue() => s_configValue;
}
上面程式碼可以通過如下AOT化執行
dotnet run -c Release -f net7.0 --filter "*" --runtimes nativeaot7.0 nativeaot8.0
BenchmarkDotNet 輸出如下
Method Runtime Mean Ratio
GetConfigValue NativeAOT 7.0 1.1759 ns 1.000
GetConfigValue NativeAOT 8.0 0.0000 ns 0.000
可以看到即使是效能測試的Benchmark,AOT優化也是不放過的。
另外還值得一提的地方就是分層,因為AOT裡面沒有分層的概念。但是即時編譯也就是不是AOT編譯的時候,一個方法從tier0提升到tier1,方法裡面的靜態欄位必須被初始化過了。AOT裡面新增了一個快速路徑檢查欄位是否初始化,避免一些不必要的開銷。
其它的一些改進,比如AOT鎖的實現方式。使用了一種混合方式,開始使用輕量級自旋鎖,後面升級到使用 System.Threading.Lock 型別,這個應該會在.NET9.0裡面釋放出來。
作者:jianghupt
原文:.NET8極致效能優化AOT
文章公眾號(jianghupt)首發,歡迎關注。