.NET8極致效能最佳化AOT

江湖評談發表於2023-12-05

前言

.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的時候表現不錯,同時也降低了總成本。


1.最佳化

在.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的最佳化魔力。


2.繼續最佳化

但是最佳化的情況遠不止如此,比如說我們可以配置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。

3.其它最佳化

當然除了體積的最佳化之外,還有其它的最佳化,比如避免了在讀取靜態欄位時的輔助呼叫,再比如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裡面釋放出來。

歡迎加入C#12.NET8最新技術交流群

結尾

作者:jianghupt
原文:.NET8極致效能最佳化AOT

文章公眾號(jianghupt)首發,歡迎關注。

相關文章