不安裝運行時運行 .NET 程式 – NativeAOT
大家好,先祝大家國慶快樂。不過大家看到這篇文章的時候估計已經過完國慶了 。
上一篇我們寫了如何通過 SelfContained 模式發布程式(不安裝運行時運行.NET程式)達到不需要在目標機器上安裝 runtime 就可以運行 .NET 程式的目標。其實除了標準的 self-contained 微軟還給我們帶來了 Native AOT 發布模式。是的你沒看錯,通過該技術我們的 .NET 程式會直接編譯為 Native 程式碼而不再是 IL ,程式運行的時候直接就是機器碼,不再需要 JIT 編譯。通過 AOT 技術,我們的程式啟動會變的非常快並且使用更少的記憶體,並且運行的時候不需要在機器上安裝任何運行時。
前階段 .NET7 發布了第一個 RC 版本,標誌著正式版的 AOT 馬上會隨 .NET7 發布而到來。所以趁著國慶趕緊體驗一把。
環境與工具
現階段 .NET7 還在RC,所以我們選擇安裝 SDK 7.0.100-rc.1.22431.12 ,作業系統是 WIN10 64位,開發工具是 VS2022 17.4.0 Preview 2.1 。正式版的 VS2022 是沒辦法選擇目標框架 .NET7 的,但是其實可以手動改 csproj 文件,所以 VS2022 Preview 不是必須的。
Console App
我們新建一個控制台程式,目標框架選擇 NET7
(如果使用正式版的 VS2022 沒有辦法選擇 net7 ,可以直接編輯 csproj 文件),右鍵項目選擇「編輯項目文件」,在 PropertyGroup
節點下添加 PublishAot
:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<!--aot發布-->
<PublishAot>true</PublishAot>
</PropertyGroup>
</Project>
修改 main 方法:
Console.WriteLine("Hello, AOT!");
Console.Read();
使用 dotnet
命令進行發布:
dotnet publish -r win-x64 -c Release
AOT 發布相比正常發布會慢一點,等待發布成功後,我們可以到以下目錄查看 bin\Release\net7.0\win-x64\publish
:
我們可以看到生成的 exe 文件只有 3.48MB ,相比普通單文件發布加裁剪過後的程式小了不少。
我們把這個 exe 程式複製到一台沒有安裝 .net 環境的伺服器上,順利運行起來了。
ASP.NET CORE
上面我們測試了一下控制台程式的 AOT 發布,相對比較簡單沒有什麼問題。下面讓我們試試應用範圍最為廣泛的 ASP.NET CORE 項目 AOT 發布行不行。
新建一個 ASP.NET CORE WebApi 項目,目標框架選擇 NET7 。同樣的操作編輯 csproj 文件,添加 PublishAot 屬性:
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<PublishAot>true</PublishAot>
</PropertyGroup>
</Project>
同樣使用 dotnet cli 命令進行發布:
dotnet publish -r win-x64 -c Release
不同於上面控制台項目的發布,ASP.NET CORE 項目的 AOT 發布會出現很多警告資訊,暫且忽略。
等到發布完成後,我們看到生成了一個 27MB 大小的 exe 文件。雙擊運行起來,不得不提一句,這個啟動速度真的是肉眼可見的快,雙擊之後瞬間就啟動了。這個就是 AOT 發布最大的優勢了。
訪問一下默認生成的那個 Action 方法://localhost:5000/WeatherForecast/ 成功的輸出了天氣資訊。
序列化的問題
以上通過簡單的測試,ASP.NET CORE WebApi 項目順利的跑起來了, 當然他只是一個簡單的示例項目,我們生產的項目相比這些要複雜多了。經過更深入的測試,發現現階段 ASP.NET CORE 進行 AOT 發布後有一個比較麻煩的問題,那就是 JSON 序列化。
以下程式碼是默認生成的 WeatherForecastController 的 GET 方法,這個方法是個標準的同步方法,進行 AOT 發布後序列化沒有任何問題。
[HttpGet]
public WeatherForecast[] Get()
{
return Enumerable.Range(1, 5).Select(index => new WeatherForecast
{
Date = DateTime.Now.AddDays(index),
TemperatureC = Random.Shared.Next(-20, 55),
Summary = Summaries[Random.Shared.Next(Summaries.Length)]
})
.ToArray();
}
但是如果把程式碼改成非同步,或者說的更直白一點的話,返回值是 Task<T
> 類型就會出現問題。比如把上面的程式碼使用 Task.FromResult 改造一下,使返回值變成 Task<WeatherForecast[]>
[HttpGet]
public async Task<WeatherForecast[]> Get()
{
var arr = Enumerable.Range(1, 5).Select(index => new WeatherForecast
{
Date = DateTime.Now.AddDays(index),
TemperatureC = Random.Shared.Next(-20, 55),
Summary = Summaries[Random.Shared.Next(Summaries.Length)]
})
.ToArray();
var result = await Task.FromResult(arr);
return result;
}
改造的程式進行 AOT 發布後運行,訪問對應的介面程式不會有任何報錯,但是返回值是個空對象的json:
{}
嘗試修復該問題,並沒有特別的好辦法,目前能夠勉強使用的辦法是使用 System.Text.Json source generator 模式進行序列化:
首先編寫一個 WeatherForecastContext 類繼承 JsonSerializerContext,並且標記為 partial。為啥要標記為 partial ?因為類的另外部分是 source generator 自動生成的。
[JsonSerializable(typeof(Task<WeatherForecast[]>))]
internal partial class WeatherForecastContext : JsonSerializerContext
{
}
第二步,在配置 services 的時候順便把 WeatherForecastContext 配置進去。
builder.Services.AddControllers()
.AddJsonOptions(options => options.JsonSerializerOptions.AddContext<WeatherForecastContext>());
通過以上操作,再次 AOT 發布後運行程式,訪問介面,數據是能正確的返回了。但是有一點小瑕疵是Task對象自身的屬性也被序列化出來了。
{
"result": [
{
"date": "2022-10-08T19:14:26.1801524+08:00",
"temperatureC": 6,
"temperatureF": 42,
"summary": "Warm"
},
{
"date": "2022-10-09T19:14:26.1816645+08:00",
"temperatureC": -9,
"temperatureF": 16,
"summary": "Bracing"
},
{
"date": "2022-10-10T19:14:26.1816648+08:00",
"temperatureC": -1,
"temperatureF": 31,
"summary": "Sweltering"
},
{
"date": "2022-10-11T19:14:26.181665+08:00",
"temperatureC": -17,
"temperatureF": 2,
"summary": "Balmy"
},
{
"date": "2022-10-12T19:14:26.1816651+08:00",
"temperatureC": -16,
"temperatureF": 4,
"summary": "Freezing"
}
],
"asyncState": null,
"creationOptions": 0,
"exception": null,
"id": 1,
"isCanceled": false,
"isCompleted": true,
"isCompletedSuccessfully": true,
"isFaulted": false,
"status": 5
}
桌面程式
以上對控制台程式,web 程式進行了測試,接下來順便對桌面 GUI 程式測試一下吧。
很遺憾,不管是 WINFROM 還是 WPF 程式,進行 AOT 發布的時候直接都會報錯,提示不支援。
一些其他限制
AOT 發布的程式會有一些限制,我們編寫的時候需要注意:
- No dynamic loading (for example, Assembly.LoadFile)
- No runtime code generation (for example, System.Reflection.Emit)
- No C++/CLI
- No built-in COM (only applies to Windows)
- Requires trimming, which has limitations
- Implies compilation into a single file, which has known incompatibilities
- Apps include required runtime libraries (just like self-contained apps, increasing their size, as compared to framework-dependent apps)
以上是直接複製的英文文檔(原文地址在文末),因為英文不是很好,不進行翻譯了,怕誤導大家。主要需要注意的就是 1,2 兩點 ,關於動態載入類庫跟動態生成程式碼的問題。我想序列化的問題大概也就是出在這裡,因為傳統的序列化需要大量的使用動態生成程式碼技術。
總結
通過以上我們對 .NET 上最常用的幾種程式進行了 Native AOT 發布的測試。總體來說控制台跟ASP.NET CORE 項目能用,WINFROM 跟 WPF 不能用。比較遺憾的有兩個點:
- ASP.NET COER 在序列化方面貌似還有點小問題。不知道是不是我環境的問題,如果有知道的大神請指點指點
- 不支援桌面 GUI 程式。其實從個人的經驗來說,桌面端可能對啟動速度更加敏感一點,因為c/s程式經常性的打開關閉、打開關閉,如果啟動慢用戶是很容易察覺的。如果桌面程式能支援 AOT ,那麼能大大改進現在 .NET 程式的啟動速度,這對用戶體驗的提升是非常大的。服務端的話本身啟動一次後就長期運行,用戶不會時時刻刻感受到啟動速度帶來的影響。另外現在 .NET 程式啟動本身就不慢,況且還有 R2R 可以選,正常在100-200ms之間的啟動速度已經對用戶體驗影響不大了。所以 AOT 之後的啟動速度的優勢不是很大。
另外來說說性能,有同學可能覺得 Native AOT 之後性能會有很大的提升,畢竟大家都迷信 Native 速度快嘛。但是經過大佬們的測試事實上 AOT 之後跟沒有 AOT 的程式碼性能基本在伯仲之間,有些地方甚至不如非 Native 的程式碼。為什麼?因為非 Native 程式碼可以進行運行時 JIT 啊,可以在運行時分析程式碼對熱點程式碼進行二次 JIT 來提升性能,而 Native AOT 之後的程式碼做不到這點。
參考
Native AOT Deployment
Try the new System.Text.Json source generator
AOT和單文件發布對程式性能的影響