.NET 的 Native AOT 是什麼 - 先釐清與 JIT、ReadyToRun、trimming 的差異
之前寫過 把 C# 透過 Native AOT 做成原生 DLL 的方法 - 使用 UnmanagedCallersOnly 從 C/C++ 呼叫,但那篇其實順序有點顛倒了。
原本應該先擺「Native AOT 到底是什麼」比較自然。
Native AOT 的話題,一開始這幾個常常會混在一起。
- 是在講去掉 JIT 的事嗎
- 和 self-contained 或 single-file 有什麼不同
- 和 ReadyToRun 是同一類嗎
- trimming warning 大量跑出來到底是什麼狀況
- WPF 或 WinForms 能不能直接套上去
這些混在一起時,Native AOT 會看起來像「只是變快的魔法」,或者反過來像「滿是限制、不能碰的東西」。
兩邊都稍嫌粗糙。
簡單說,Native AOT 是 把 .NET 應用在 publish 時相當靜態地固化下來的發布模式。
代價是對啟動與發布層面效果明顯。
反過來,想在執行期間動態解決各種事情的程式,就比較不合拍。
這篇文章就從實務面來整理這一點。
1. Native AOT 做了什麼
平時的 .NET 會先產生 IL,再在執行期對需要的地方做 JIT。
Native AOT 則是把「執行期才產生原生碼」這一段大幅提前到 publish 時。
所以在外觀上,它是朝著「把 .NET 包得像原生應用一樣發布」的方向。
這裡重要的是 希望在 publish 時就能看到絕大多數必要的程式碼。
也就是說,
- 執行期才尋找型別
- 執行期才長出程式碼
- 執行期才讀取組件來判斷
這類寫法,無論如何都會比較不合。
若把 Native AOT 只看成「變快的功能」,在這裡就會偏掉。
本質上更像是 捨棄一些動態世界,往靜態容易固化的世界靠攏。
2. 和 ReadyToRun 差在哪裡
先把這裡分清楚會輕鬆很多。
| 觀點 | 一般 JIT | ReadyToRun | Native AOT |
|---|---|---|---|
| 執行期 JIT | 使用 | 仍會使用 | 不使用 |
| 發布產物 | 以 IL 為主 | IL + 事先產生的原生碼 | 以原生可執行檔為主 |
| 啟動 | 基準 | 容易改善 | 更容易改善 |
| 相容性 | 廣 | 相對廣 | 限制較多 |
ReadyToRun 是把 JIT 的工作稍微提前的方向。
Native AOT 則是 不以執行期 JIT 為前提 的方向。
名字看起來接近,實際手感卻差不少。
ReadyToRun 是「先讓啟動稍微輕一點」。
Native AOT 是「為了啟動、發布、執行環境的需求,設計也往靜態一點靠」。
3. 用了 Native AOT 有什麼好處
最直觀的還是啟動。
在 CLI 工具、短命行程、容器裡的小 API、會常駐但功能受限的工具上,JIT 的開銷會很顯眼。
Native AOT 把這部分先做完,起手就會比較輕。
另一個是發布層面。
在不想要求「請先在機器上安裝 .NET Runtime」的情境中,心裡會輕鬆不少。
- 只想發布一支小工具
- 希望容器映像盡量小
- 不想在執行環境上裝 JIT
這種場景下,Native AOT 用得上的位置很明確。
所以 Native AOT 適合的不是「所有應用」,而是 想減少啟動、發布、執行環境前提條件的應用。
4. 反過來哪些地方會變辛苦
這裡答案相當明確。
首先是反射與動態產生程式碼。
Assembly.LoadFileSystem.Reflection.Emit- 執行期把型別逐一列舉出來尋找的結構
- 從字串名稱查型別再實體化的機制
這些在 publish 時不容易把必要程式碼固定下來,就會成為 AOT warning 的溫床。
Native AOT 噴一堆 warning 時,與其覺得「吵死了」,倒不如讀成「這個設計在 publish 時就是看不清楚」。
特別是 RequiresDynamicCode 系列,通常值得認真看。
再來是 trimming。
Native AOT 和 trimming 綁得相當緊。
這裡麻煩的是,不只自己的程式碼,連相依函式庫的寫法都會受影響。
例如,
- 以反射為基礎的序列化器
- 以執行期掃描為前提的 DI
- 使用動態 proxy 的函式庫
這些都容易讓事情變麻煩。
換句話說,做 Native AOT 的時候,與其「執行期聰明處理」,不如 往建置期就能看清楚的形式靠攏 比較好控制。
5. Windows 桌面端要稍微謹慎一點
在 Windows 上,Native AOT 沒有 built-in COM。
再加上 WPF 和 trimming 的搭配並不好,WinForms 也相當仰賴 built-in COM marshalling。
因此,
- 直接把 WPF / WinForms 主體丟去 Native AOT
- 把 COM interop 以平常的感覺直接帶入
這些都要謹慎看。
反過來,比較適合當入口的是,
- Console
- worker
- 小型 Web API
- 職責分明的小元件
。
Native AOT 並不是「.NET 的東西都能直接套上去」。
尤其在 Windows 桌面與 COM 味濃的世界中,很多情境還是維持一般 JIT 會比較順。
6. 最小步驟其實很簡單
在專案端,先在 csproj 裡加上 PublishAot。
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<PublishAot>true</PublishAot>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
</Project>
publish 的指令,例如 Windows x64 會是這樣。
dotnet publish -c Release -r win-x64
Linux x64 則是,
dotnet publish -c Release -r linux-x64
走到這一步,就已經相當「像原生應用的世界」了。
由於 RID 是固定的,產物比起「同一支 DLL 到哪都能跑」,更像是為該 OS / 架構量身製作的執行檔。
另外還有一個意外常中獎的地方是 JSON。
與其用平常的方式把 System.Text.Json 寫成靠反射的路徑,不如靠 source generation,比較不容易出事。
using System.Text.Json;
using System.Text.Json.Serialization;
[JsonSerializable(typeof(AppConfig))]
internal partial class AppJsonContext : JsonSerializerContext
{
}
public sealed class AppConfig
{
public string? Name { get; init; }
public int RetryCount { get; init; }
}
var config = new AppConfig
{
Name = "sample",
RetryCount = 3
};
string json = JsonSerializer.Serialize(config, AppJsonContext.Default.AppConfig);
與其說是為了 Native AOT,不如記成 改往「不要讓執行期去找型別」的寫法靠,比較不會偏掉。
7. 從哪裡開始試比較好
第一個對象,下列幾類比較容易上手。
- 啟動很重要的 CLI
- worker
- 小型 Web API
- 在原生整合中職責較窄的元件
這些比較容易減少動態機制,也比較容易看到 Native AOT 的好處。
反之,WPF / WinForms 既有的大型應用主體、以 plugin 載入為核心的結構、重度依賴 COM 的結構,做為首波對象並不太和平。
8. 總結
Native AOT 是在 publish 時把 .NET 做得相當靜態的機制。
- 對啟動有效
- 發布也會變輕鬆
- 代價是對動態機制會變嚴苛
先掌握這三點,視野會清楚很多。
Native AOT 不是「所有 .NET 應用都該套上的標配開關」。
但在啟動重要、想讓發布更輕、想降低對執行環境的前提要求時,它是相當強的選項。
反過來,在 Windows 桌面或 COM 味濃的世界中,繼續走一般 .NET 往往比較順。
能分得出這兩邊之後,Native AOT 就不再是「難搞的新功能」,而是位置清楚的工具。
9. 參考資料
- Native AOT deployment overview - .NET
- Native AOT deployment overview - .NET (日本語)
- Introduction to AOT warnings - .NET
- Prepare .NET libraries for trimming - .NET
- Known trimming incompatibilities - .NET
- How to use source generation in System.Text.Json - .NET
- ASP.NET Core support for Native AOT
- ReadyToRun deployment overview - .NET
- Building native libraries - .NET
- ComWrappers source generation - .NET
- 相關文章:把 C# 透過 Native AOT 做成原生 DLL 的方法 - 使用 UnmanagedCallersOnly 從 C/C++ 呼叫
- 相關文章:要在 C# 中使用原生 DLL,C++/CLI 包裝是有力選項的理由 - 與 P/Invoke 的比較整理
相關文章
共用相同標籤的最新文章。能以相近的主題延伸理解。
.NET 的 Generic Host 是什麼 - 先整理 DI、設定、日誌、BackgroundService
本文整理 .NET Generic Host 的真面目,並把 DI、設定、日誌、IHostedService 與 BackgroundService 的關係,連同 Host.CreateApplicationBuilder 與 WebApplicationBuilder 的...
PeriodicTimer / System.Threading.Timer / DispatcherTimer 的區分使用 - 先整理 .NET 的定期執行
整理 .NET 中 PeriodicTimer、System.Threading.Timer、DispatcherTimer 的差異與使用場景,從執行緒、async 流程、callback 重疊三個角度切入,協助你在 worker、ThreadPool 背景處理及 WPF ...
用 Native AOT 把 C# 做成原生 DLL 的方法 - 用 UnmanagedCallersOnly 從 C/C++ 呼叫
從現有 C/C++ 應用程式以 in-process 方式呼叫 C# 邏輯時,本文示範以 Native AOT 將類別庫發佈為原生 DLL,並用 UnmanagedCallersOnly 公開 cdecl 進入點。透過 handle、錯誤碼與扁平 C ABI 設計交界面,整...
把 Generic Host / BackgroundService 帶進桌面應用程式的理由 - 啟動・壽命・graceful shutdown 的整理會輕鬆很多
整理把 .NET Generic Host 與 BackgroundService 帶進 WPF / WinForms 桌面應用程式的理由,把啟動、lifetime、graceful shutdown 集中於入口管理。透過 StartAsync / ExecuteAsync...
FileSystemWatcher 的使用方法與注意事項 - 漏掉、重複通知、完成判定的陷阱
本文整理 FileSystemWatcher 的正確用法。把事件視為跡象而非完成通知,將通知摺疊為重新掃描請求,由傳送端以 temp 後 rename 明示完成,多 worker 以原子性 claim 取得所有權,最後以 full rescan 與 idempotency ...
相關主題
與本文相近的主題頁面。以本文為起點,可進一步連到相關服務與其他文章。
Windows 技術主題
彙整 KomuraSoft LLC 關於 Windows 開發、故障調查與既有資產活用文章的主題中心。
32 位元 / 64 位元互通
整理 32 位元 / 64 位元互通、原生邊界與相關 Windows 設計判斷的主題頁面。
與本主題相關的服務
本文連結到以下服務頁面,歡迎從最接近的入口查看。
Windows 應用程式開發
支援包含常駐處理、設備連動、運作日誌與可維護結構的 Windows 桌面應用程式。
技術諮詢 & 設計審查
協助釐清設計方向、架構邊界、生命週期責任,以及既有 Windows 資產的處理方式。