.NET Native AOT 是什麼 - 與 JIT、trimming 的差異

· 更新日期: · · C#, .NET, Native AOT, 發行, 設計

更新紀錄(1 筆,最後更新 2026年09月04日)

本文的修改紀錄。已保存的更新前版本,可透過附有 DOI 的永久連結閱讀。

已將繁體中文版改寫為日文原文的完整翻譯。先前的繁體中文版只譯出日文原文的一部分,遺漏了章節、表格、Mermaid 圖、圖說與 FAQ。本次依日文原文將這些內容全部補回,並新增本文的知識地圖章節。技術主張與日文版一致。 查看更新前的版本 (DOI: 10.5281/zenodo.21616272)
初次發布
引用本文(DOI: 10.5281/zenodo.21616271)

本文保存於 Zenodo。以下同時提供一律指向最新版本的 DOI,以及固定於您正在閱讀版本的 DOI。

Go Komura(2026)。〈.NET Native AOT 是什麼 - 與 JIT、trimming 的差異〉。小村軟體有限公司。https://doi.org/10.5281/zenodo.21616271 https://comcomponent.com/zh-TW/blog/2026/03/13/001-dotnet-native-aot-what-is/

DOI(最新版本)
10.5281/zenodo.21616271
DOI(此版本)
10.5281/zenodo.22297109

之前在 從 C/C++ 呼叫 C# Native AOT DLL 的方法 裡,已經寫過用 Native AOT 從 C/C++ 呼叫 C# 的做法。 不過說實在的,先擺一篇 Native AOT 到底是什麼 會比較貼心。順序稍微顛倒了一點。

Native AOT 這個話題,一開始最容易把用語攪在一起。

  • 這是在講把 JIT 拿掉嗎
  • 和 self-contained 或 single-file 差在哪裡
  • 和 ReadyToRun 是同一個系統嗎
  • trimming warning 大量冒出來時到底發生了什麼事
  • WPF / WinForms / ASP.NET Core 是不是都能用同樣的認真程度去套

這些一旦攪在一起,Native AOT 就會看起來像「只是變快的魔法」,或者反過來像「滿是限制又很可怕的東西」。兩種看法都有點草率。

用語攪在一起時產生的誤解JIT、self-contained、ReadyToRun、trimming 這些用語一旦攪在一起,Native AOT 就會看起來像只是變快的魔法或滿是限制的可怕東西,但兩種看法都很草率的圖。用語攪在一起看起來像變快的魔法看起來滿是限制很可怕把用語分開梳理變成看得見使用時機的選項

圖 1: 誤解的來源是用語混線。先從把用語分開開始。

這篇文章以 .NET 8 之後的現況實務感受為前提,先把下面四點梳理清楚。

  • Native AOT 的真面目
  • 好處在哪裡,哪裡會變吃力
  • 和 ReadyToRun、trimming 差在哪裡
  • 從哪一種應用程式開始試比較平穩

目錄

  1. 先講結論(一句話)
  2. 先看的整理表
    • 2.1. Native AOT 周邊的用語
    • 2.2. JIT / ReadyToRun / Native AOT 的差異
  3. Native AOT 的全貌(圖)
  4. Native AOT 的好處
    • 4.1. 啟動容易變輕
    • 4.2. 不必以事先安裝執行階段為前提
    • 4.3. 適合受限的執行環境
  5. Native AOT 會變吃力的地方
    • 5.1. 反射與動態產生程式碼
    • 5.2. 必須以 trimming 為前提思考
    • 5.3. 依平台分別發行
    • 5.4. Windows 桌面 / COM 情境要相當謹慎
  6. 最小步驟
    • 6.1. csproj
    • 6.2. publish
    • 6.3. JSON 的寫法
  7. 適合的情境
  8. 不適合的情境
  9. 常見的卡關處
  10. 總結
  11. 參考資料

圖中實線表示始終成立的關係,虛線表示附帶條件的關係(成立條件寫在詳細頁面中各關係的說明中)。關係的完整清單(共 16 條,附依據與可信度)以及主要概念的定義,彙整在知識地圖詳細頁面(日文)。資料:JSON-LD / Turtle

1. 先講結論(一句話)

  • Native AOT 是在 publish 時把 .NET 應用程式事前編譯成原生程式碼再散發的方式。
  • 因為不使用執行階段 JIT,啟動時間與記憶體占用容易變好,也比較容易散發到沒有安裝 .NET 執行階段的環境。
  • 不過,它和自由的反射、動態產生程式碼、built-in COM,以及不支援 trimming 的函式庫都合不來。
  • 也就是說,與其說是變快的魔法,不如說是為了啟動、散發與執行環境的需求,捨掉一部分動態世界而往靜態世界靠攏的發行模型。

Native AOT 是「把 .NET 用接近原生的方式散發出去的機制」,並不是一個打勾就讓編譯變快的核取方塊。

得到什麼與捨棄什麼Native AOT 透過 publish 時的事前編譯得到啟動時間、記憶體占用與不需執行階段的散發,代價是捨棄自由的反射、動態產生程式碼與 built-in COM 的相容性的圖。Native AOT得到的東西捨棄的東西啟動、記憶體與散發都變輕自由的反射動態產生程式碼與 built-in COM

圖 2: 不是變快的魔法,而是捨掉一部分動態世界而往靜態世界靠攏的交換。

2. 先看的整理表

2.1. Native AOT 周邊的用語

一開始先把這幾個分開,後面會輕鬆很多。

用語 做什麼用的 與 Native AOT 的關係
JIT 在執行階段從 IL 產生原生程式碼 Native AOT 把這一段事先做完
self-contained 把執行所需的 .NET 一整套一起散發 Native AOT 屬於這個系統
single-file 把散發產物合併成 1 個檔案 和 Native AOT 的本質是兩回事,但結果的外觀容易變得接近
trimming 削掉沒有用到的程式碼 在 Native AOT 幾乎是前提
ReadyToRun 保留 IL,只把 JIT 的工作稍微提前 和 Native AOT 看似相近,其實是另一回事
source generator 把執行階段的動態處理移到建置期產生程式碼 和 Native AOT 很搭

容易混淆的地方在於,Native AOT 與其說是一個功能,不如說是一種發行模型:它會和 self-contained、trimming、source generation、固定 RID 的 publish 搭在一起運作。

和 Native AOT 一起運作的機制Native AOT 不是單一功能,而是和 self-contained、trimming、source generation、固定 RID 的 publish 組合起來運作的發行模型的圖。Native AOT〔發行模型〕self-contained 的系統trimming 幾乎是前提和 source generator 很搭以固定 RID 進行 publish

圖 3: Native AOT 不是單獨的功能,而是和周邊機制成套運作的發行模型。

2.2. JIT / ReadyToRun / Native AOT 的差異

這裡也是一開始用一張表看比較快。

觀點 一般的 JIT 執行 ReadyToRun Native AOT
執行階段 JIT 使用 仍有會用到的場合 不使用
散發產物的內容 以 IL 為主 IL 加上事先產生的程式碼 以原生執行檔為主
啟動 基準 容易改善 相當容易改善
相容性 最廣 廣 限制較強
動態功能 好用 大致上好用 限制很多
適合的目的 一般的 .NET 開發 想先改善啟動 想強力拿下啟動、散發與受限環境

如果說 ReadyToRun 走的是「讓 JIT 輕鬆一點」的方向,那麼 Native AOT 走的就是「不以執行階段 JIT 本身為前提」的方向。 即使名稱裡都帶著 AOT,兩者的認真程度差很多。

ReadyToRun 和 Native AOT 的方向差異ReadyToRun 保留 IL 只把 JIT 的工作稍微提前讓它輕鬆一點,Native AOT 則不以執行階段 JIT 本身為前提,即使都叫 AOT 認真程度也差很多的圖。讓它輕鬆一點不以它為前提想怎麼處理 JITReadyToRun〔IL 保留下來〕Native AOT〔以原生為主〕相容性維持得很廣啟動快但限制強

圖 4: 同樣叫 AOT,「讓 JIT 輕鬆」和「不以 JIT 為前提」是兩回事。

在這個基礎上,把「那到底要選哪一種散發形態」畫成一張圖,會是這樣。

能裝沒有到那個程度想壓低不想裝 / 裝不了依賴不依賴是否想決定散發形態能不能在散發目的地裝 .NET 執行階段?想壓低啟動時間嗎?framework-dependent一般的發行ReadyToRun維持相容性同時改善啟動是否依賴 reflection、動態產生程式碼、built-in COM?self-contained必要時用 single-file 合併對象是 console / worker / 小型 API?Native AOT先 self-contained減少動態相依之後再重新評估

圖 5: 散發形態的選法。依序判斷能不能放執行階段,再看能不能減少動態機制。

第一個分歧是 能不能把執行階段放到散發目的地,第二個分歧是 動態機制能減少到什麼程度。 self-contained 和 single-file 講的是「怎麼散發」,ReadyToRun 和 Native AOT 講的是「什麼時候產生原生程式碼」,實務上會把兩邊組合起來考慮。

3. Native AOT 的全貌(圖)

把 Native AOT 粗略地畫成圖,會是這樣。

一般執行dotnet publish + PublishAotC# / .NET 原始碼IL 組件執行階段 JIT應用程式執行AOT / trim 分析削減不需要的程式碼產生原生程式碼RID 專屬的執行檔

圖 6: 一般執行是在執行階段做 JIT,Native AOT 則把分析、削減與原生產生提前到 publish 時。

平常的 .NET 會先產生 IL,再在執行階段只對需要的部分做 JIT。 Native AOT 則把後段很大一部分提前到 publish 時完成。

這時重要的是,在 publish 的時間點必須「幾乎完全知道執行階段會用到哪些程式碼」。 到這裡,允許的寫法前提就變了。

  • 在執行階段尋找型別
  • 在執行階段長出程式碼
  • 在執行階段載入 Assembly
  • 抱著「總會有辦法」的心態延後解析

這類寫法會突然變得和 Native AOT 很不合。

publish 時必須全部知道Native AOT 在 publish 的時間點必須幾乎完全知道執行階段會用到哪些程式碼,因此在執行階段尋找型別、長出程式碼、載入 Assembly 或延後解析的寫法都會變得很不合的圖。publish 時確定所需程式碼在執行階段尋找型別在執行階段長出程式碼在執行階段讀取 Assembly靠延後解析撐過去每一種都和 AOT 不合

圖 7: 前提改變的核心。越是「在執行階段才決定」的寫法,越會和 Native AOT 衝突。

4. Native AOT 的好處

4.1. 啟動容易變輕

Native AOT 最容易看出來的效果,還是啟動。

  • CLI 工具
  • 短命處理程序
  • 類似無伺服器的啟動
  • 容器的啟動與替換
  • 監控工具或小型常駐處理程序

在這些場合 JIT 的成本很明顯,Native AOT 把那一段提前做完,起步就會比較輕。

記憶體占用也容易改善,所以在想把同樣台數塞得更滿的場合也有利。 特別是在同一個處理程序會大量啟動的雲端側,這個差距會逐漸拉開。

4.2. 不必以事先安裝執行階段為前提

用 Native AOT publish 的應用程式,在沒有安裝 .NET 執行階段的環境也比較容易跑起來。

這一點不起眼卻很有用。

  • 不想跟散發目的地說「請先安裝 .NET 9 Runtime」
  • 想讓容器映像檔更精簡
  • 想只放一支小工具就讓它動起來
  • 不想/不能在執行環境上允許 JIT

在這些場合,光是少了「要另外備妥執行階段」這個前提,事情就安靜很多。

這裡說的「不需要執行階段」,意思是不必在散發目的地另外安裝 .NET。 並不是說應用程式裡面連相當於 runtime 的部分都完全消失。

不需要執行階段的正確意思Native AOT 所說的不需要執行階段是指不必在散發目的地另外安裝 .NET,並不是指應用程式裡面相當於 runtime 的部分完全消失的圖。不需要執行階段這句話不必在散發目的地安裝 .NET並不是相當於 runtime 的部分消失散發與啟動的前提變少

圖 8: 「不需要執行階段」講的是散發目的地。相當於 runtime 的部分仍包含在應用程式裡。

4.3. 適合受限的執行環境

Native AOT 不使用執行階段 JIT,所以在不允許 JIT 的環境也比較容易跑起來。

這一點比起 desktop,在雲端、容器、行動裝置這一側更容易發揮。 不過在 Windows 開發的情境裡,就「減少散發目的地多餘的前提」這個意義來說,也是相當令人開心的優點。

5. Native AOT 會變吃力的地方

5.1. 反射與動態產生程式碼

Native AOT 最核心的限制就在這裡。

  • Assembly.LoadFile 這類動態載入
  • System.Reflection.Emit 這類執行階段產生程式碼
  • 在執行階段無限制地追型別的反射
  • 在執行階段隨意組出 generic 的寫法

這些在 publish 時不容易確定需要哪些程式碼,所以是 AOT warning 的溫床。

當然,也沒有單純到「只要有一行反射就立刻出局」。 不過,越是偏向「在執行階段看了再決定」的設計就越吃力,這個傾向不會落空。

warning 名稱裡常出現的是 RequiresDynamicCode 系列。 它的意思是「這個呼叫在 AOT 下可能會壞掉」,所以不要隨手 suppress 比較安全。

做 Native AOT 的時候,可以把它想成是減少「執行階段的聰明」、增加「建置期的明示」的方向,這樣比較好理解。

從執行階段的聰明轉向建置期的明示動態載入、執行階段產生程式碼與無限制的反射都是 AOT warning 的溫床,因此要減少執行階段的聰明、增加建置期的明示,把設計往那個方向靠攏的圖。依賴執行階段聰明的設計RequiresDynamicCode 系列的 warning增加建置期的明示publish 時確定所需程式碼不要隨手 suppress

圖 9: 面對核心限制的方向只有一個。把「在執行階段看了再決定」換成「在建置期明示」。

5.2. 必須以 trimming 為前提思考

Native AOT 和 trimming 綁得很深。 這裡容易漏看的是,不只自己的程式碼,相依函式庫那一側的寫法也會產生影響。

要特別注意的是這幾類。

  • 以反射為基礎的序列化器
  • 以執行階段掃描收集型別的 DI/外掛結構
  • 從字串名稱找型別再建立執行個體的機制
  • 偏向動態 proxy 或 IL 產生的函式庫

明明出現了 warning 卻覺得「publish 過了就好」,之後會很難收拾。 在 Native AOT,warning 大致上都值得認真讀。

trimming 的成敗不是只看自己的程式碼trimming 不只自己的程式碼,相依函式庫那一側的寫法也會產生影響,以反射為基礎的序列化器、執行階段掃描的 DI、從字串名稱解析型別、偏向動態 proxy 的函式庫都要特別注意的圖。trimming 的成敗自己的程式碼寫法相依函式庫的寫法反射系或執行階段掃描系認真讀 warning 並確認

圖 10: 容易漏看的是相依函式庫那一側。「publish 過了就好」之後會很難收拾。

話雖如此,只講「認真讀」也動不了,所以這裡寫下處理的優先順序。 官方文件也建議照這個順序去試。

順序 處理方式 使用場合
1 不要用 reflection 可以把 Activator.CreateInstance(Type) 換成 generic 引數,或是改用 source generator
2 加上 DynamicallyAccessedMembers 需要 reflection,但對象型別在編譯時就已經知道
3 加上 RequiresUnreferencedCode 型別名稱是用執行階段的字串決定,本質上無法做靜態分析
4 用 UnconditionalSuppressMessage 抑制 上面全都做不到,而且已確認安全時的最後手段

第一步最常見的是第 2 種「型別明明已經知道,卻只是冒出警告」的情況。 例如下面的程式碼會出現 IL2070。

// IL2070: 'this' argument does not satisfy 'DynamicallyAccessedMemberTypes.PublicMethods'
void PrintMethodNames(Type type)
{
    foreach (var method in type.GetMethods())
    {
        Console.WriteLine(method.Name);
    }
}

只要用屬性宣告「既然要呼叫 GetMethods(),就請保留 public 方法」,警告就會消失。

using System.Diagnostics.CodeAnalysis;

void PrintMethodNames(
    [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] Type type)
{
    foreach (var method in type.GetMethods())
    {
        Console.WriteLine(method.Name);
    }
}

// 呼叫端如果是用 typeof 傳入,需求就會自動被滿足
PrintMethodNames(typeof(DateTime));

呼叫的 API 和需要的指定大致上是對應的。GetMethod / GetMethods 對應 PublicMethods,GetProperty / GetProperties 對應 PublicProperties,Activator.CreateInstance 對應 PublicParameterlessConstructor 或 PublicConstructors。 DynamicallyAccessedMemberTypes.All 雖然輕鬆,但會把對象型別的成員整批保留而讓體積膨脹,保留下來的成員也會帶出別的 warning,所以原則是 只指定必要的最小範圍。

另外,加了屬性 warning 還是不消失時,要 從使用 reflection 的地方往呼叫端回溯,確認整條路徑上都有加。中間只要漏掉一處,需求就會在那裡斷掉。

加了屬性 warning 還是不消失時DynamicallyAccessedMembers 要指定必要的最小範圍,warning 不消失時要從使用 reflection 的地方往呼叫端回溯,確認整條路徑上都有加屬性,只要漏掉一處需求就會在那裡斷掉的圖。加了屬性 warning 仍留著往呼叫端回溯確認整條路徑上都有加漏掉一處需求就會斷掉指定必要的最小範圍是原則

圖 11: 屬性要加在整條路徑上而不是單一點。中間漏掉一處,需求就會在那裡斷掉。

5.3. 依平台分別發行

Native AOT 是以固定 RID(Runtime Identifier)的方式 publish。 也就是說,這不是把為 win-x64 做出來的東西直接拿到 linux-x64 上跑的世界。

  • Windows x64
  • Windows Arm64
  • Linux x64
  • Linux Arm64
  • macOS Arm64

要以像這樣為每個目標分別產生發行物為前提。

這裡的感覺會比一般 framework-dependent 的 .NET,更接近「原生應用程式」。

5.4. Windows 桌面 / COM 情境要相當謹慎

在 KomuraSoft 的情境裡,這一節特別重要。

在 Windows 上,Native AOT 沒有 built-in COM。 再加上 WPF 和 trimming 合不來,WinForms 又重度依賴 built-in COM marshalling,所以至少在現階段,兩者作為「第一個 Native AOT 候選」都應該相當謹慎看待。

這裡先寫下所謂「現階段」的基準。 本文的敘述是以 .NET 8 / 9 / 10 世代的官方文件(截至 2026 年 7 月)為依據。 關於 WPF 和 WinForms,Microsoft 的「Known trimming incompatibilities」明確寫著:WPF 對反射與執行階段程式碼檢查的依賴很強,trimming 之後幾乎跑不動;WinForms 則對 built-in COM marshalling 的依賴很重,因此 兩者都在 .NET SDK 這一側停用了 trimming 支援。 也就是說,現況不是「最好謹慎看待」,而是 SDK 已經把它擋下來了。將來這段敘述有可能改變,所以請在閱讀的當下確認同一個頁面。

WPF 和 WinForms 被擋下來的理由Windows 的 Native AOT 沒有 built-in COM,WPF 因為依賴反射與執行階段程式碼檢查在 trimming 之後幾乎跑不動,WinForms 則對 built-in COM marshalling 的依賴很重,因此兩者都在 .NET SDK 這一側停用了 trimming 支援的圖。Windows 的 Native AOT沒有 built-in COMWPF對反射的依賴很強WinForms對 COM marshalling 的依賴很重SDK 這一側停用 trimming

圖 12: 不是「謹慎看待」,現況是 SDK 已經擋下來。不要把桌面本體當第一個候選。

總之,

  • 直接把 WPF / WinForms 本體 Native AOT 化
  • 用平常的感覺把 COM interop 原封不動帶進來

這些做法會讓 publish 時的 warning 與執行階段的限制一口氣增加,處理成本容易暴增。

反過來,

  • 主控台
  • worker
  • 小型 Web API
  • 原生介接之中比較容易收攏到 C 函式邊界的元件

這些當作入口比較直接了當。

如果需要 COM,維持 JIT,或是以 ComWrappers / source-generated COM 為前提重新設計,有些情況下方向會比較對。

6. 最小步驟

在那之前,Native AOT 的 publish 需要原生的工具鏈。 只加上 PublishAot 就打 dotnet publish,會失敗在最後的原生連結,而不是編譯。這是第一道關卡,所以請先裝好。

環境 需要的東西
Windows Visual Studio 2022 以後版本。安裝「使用 C++ 的桌面開發」工作負載,並把預設的元件全部裝上
Ubuntu 18.04 以後 sudo apt-get install clang zlib1g-dev
Alpine 3.15 以後 sudo apk add clang build-base zlib-dev
Fedora 39 以後 / RHEL 8 以後 sudo dnf install clang zlib-ng-devel zlib-ng-compat-devel zlib-devel
macOS Xcode 的 Command Line Tools(.NET 8 以後支援)

總之就是編譯器工具鏈,以及 .NET 執行階段所依賴的函式庫的開發用套件。 如果要在 CI 上跑,dotnet/samples 的 Native AOT 範例裡有 Linux 和 Windows 兩邊的 Dockerfile,從那裡把前提的安裝步驟搬過來最快。

另外,在 Linux 上建置的二進位檔 只能在相同或更新的 Linux 上執行。在 Ubuntu 20.04 做出來的東西在 20.04 以後可以跑,但在 18.04 不能跑。建置環境的選法直接就決定了可散發的範圍。

publish 前的關卡與可散發範圍Native AOT 的 publish 需要原生工具鏈,沒有的話會在最後的原生連結失敗,而且在 Linux 上建置的二進位檔只能在相同或更新的 Linux 上執行,建置環境的選法就決定了可散發範圍的圖。沒有有有沒有工具鏈在原生連結階段失敗產出各 RID 的二進位檔Linux 只能在相同或更新的環境執行建置環境的選法就是散發範圍

圖 13: 第一道關卡是工具鏈。在 Linux 上,建置環境的新舊決定了可散發範圍。

6.1. csproj

首先在專案檔裡放入 PublishAot。

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net8.0</TargetFramework>
    <PublishAot>true</PublishAot>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
  </PropertyGroup>
</Project>

範例用 net8.0 就夠了。想法本身在 .NET 9 / 10 也幾乎一樣。

重要的是,與其只在 dotnet publish 的命令列上臨時加,不如 平常就把它放在專案裡,日常就看得到 build / publish 時的分析。

另外,就算放了 <PublishAot>true</PublishAot>,平常在本機執行也不會突然變成 Native AOT。 日常的 dotnet run 與一般執行都是 JIT,Native AOT 編譯的正戲是在 publish 時。

放入 PublishAot 之後的日常就算把 PublishAot 放進專案,日常的 dotnet run 仍然是 JIT,Native AOT 編譯的正戲是在 publish 時跑,所以平常就把設定放在專案裡並日常查看 build 與 publish 時的分析很重要的圖。在 csproj 放入 PublishAot日常的 dotnet run 仍是 JITpublish 時進行 AOT 編譯日常查看分析與 warning

圖 14: 日常是 JIT,正戲在 publish。正因如此,設定不要臨時加,而要常駐並持續查看分析。

6.2. publish

例如要給 Windows x64 用,會是這樣。

dotnet publish -c Release -r win-x64

要給 Linux x64 用的話,是這樣。

dotnet publish -c Release -r linux-x64

輸出是固定 RID 的。 看法會從「一支到哪裡都能跑的 .NET DLL」,變成「為那個 OS / 架構做出來的執行檔」。

如果是從 Web API 那一邊入手,從以 Native AOT 為前提的範本開始比較輕鬆。

dotnet new webapiaot -o MyFirstAotWebApi

worker 的話,是這個。

dotnet new worker -o WorkerWithAot --aot

6.3. JSON 的寫法

在 Native AOT 上不起眼卻很常撞到的是 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. 適合的情境

Native AOT 用起來特別順手的,是這幾種情境。

  • 以啟動為主角的 CLI / 工具
  • 在容器裡大量部署的小型 API
  • worker / 背景服務
  • 無伺服器或短命處理程序
  • 插進原生應用程式裡的小型 .NET 元件
  • 不想要求執行環境事先安裝 .NET 執行階段的場合

共通點是 邊界比較清楚,動態機制也比較容易減少。

8. 不適合的情境

反過來,也有一開始就不該把 Native AOT 當主戰場的明確場合。

  • WPF / WinForms 的既有大型應用程式本體
  • 以 built-in COM interop 為前提的結構
  • 以執行階段載入 plugin 為主角的應用程式
  • 重度依賴以 reflection 尋找型別的框架的結構
  • 把 System.Reflection.Emit 或動態 proxy 當成理所當然在用的函式庫
  • 中間夾了 C++/CLI 的設計

這些情況下,以 JIT 為前提的一般 .NET、ReadyToRun,或是重新檢視設計上的職責劃分,方向會比較對。

9. 常見的卡關處

最後整理一下 Native AOT 初次上手容易踩到的點。

  • 輕看 publish warning
    • 如同前述,Native AOT 的 warning 是要認真讀的對象。
  • build 過得了,publish 卻壞掉
    • publish 時會連相依函式庫都納入認真分析,所以有些問題到這裡才第一次看得見。
  • 用同樣的心情看待 ReadyToRun 和 Native AOT
    • 名字相近,但限制的強度差很多。
  • 一開始就從桌面應用程式本體下手
    • 先從主控台 / worker / 小型 Web API 開始會比較平穩。
  • JSON 或設定繫結用平常的感覺寫
    • 以反射為前提的寫法,之後才會顯現出來。
  • 以為和平台無關就直接散發
    • Native AOT 的散發產物是固定 RID 的。
  • 以為「Native AOT 就是什麼都變快」
    • 主角是啟動、散發與執行環境。這裡搞錯,期待值就會偏掉。

在 Native AOT,最終決定可不可行的不是 dotnet build 而是 dotnet publish。 早一點開始跑這一段,後半就比較不會卡住。

決定可不可行的是 publishbuild 過得了卻在 publish 壞掉,是因為 publish 時會連相依函式庫都納入認真分析,Native AOT 最終決定可不可行的不是 dotnet build 而是 dotnet publish 的圖。dotnet build 過得了可不可行還沒決定dotnet publish連相依項目一起認真分析到這裡才第一次看得出可不可行所以要早一點跑 publish

圖 15: 卡關處的共通點。不要因為「build 過了」就放心,要早一點跑 publish。

10. 總結

Native AOT 用一句話講,就是 把 .NET 應用程式從動態的執行模型,往靜態上容易確定的散發模型靠攏的機制。

想先掌握的重點,就這五個。

  1. Native AOT 在 publish 時把程式事前編譯成原生程式碼
  2. 對啟動、記憶體與散發上的需求很有效
  3. 代價是對 reflection、動態產生程式碼、built-in COM、不支援 trimming 的程式碼很嚴苛
  4. 第一個對象,比起桌面本體,主控台 / worker / 小型 Web API 會平穩得多
  5. 一邊消掉 warning,一邊以 publish 為基準早一點確認很重要

Native AOT 不是所有 .NET 應用程式都要打開的標準開關。 不過在 啟動很重要、想讓散發變輕、想減少對執行環境的前提要求 的場合,它是相當強的武器。

反過來,在 WPF / WinForms / COM 味道濃厚的世界裡,一般的 .NET 方向比較對的情況仍然很多。 能分辨這兩邊之後,Native AOT 就不再是「難搞的新功能」,而是使用時機清楚的選項。

11. 參考資料

共用相同標籤的最新文章。能以相近的主題延伸理解。

與本文相近的主題頁面。以本文為起點,可進一步連到相關服務與其他文章。

本文連結到以下服務頁面,歡迎從最接近的入口查看。

常見問題

整理諮詢這個主題時常見的問題。

Native AOT 是什麼?
它是一種發行方式:在 publish 時就把 .NET 應用程式事前編譯成原生程式碼再散發。因為不使用執行階段 JIT,啟動時間與記憶體占用容易變好,也比較容易散發到沒有安裝 .NET 執行階段的環境。代價是它和自由的反射、動態產生程式碼、built-in COM,以及不支援 trimming 的函式庫都合不來。與其說是變快的魔法,不如說它是為了啟動、散發與執行環境的需求,捨掉一部分動態世界而往靜態世界靠攏的發行模型。
Native AOT 和 ReadyToRun 有什麼不同?
ReadyToRun 是保留 IL,只把 JIT 的工作稍微提前的方式,執行階段仍有會用到 JIT 的場合。它的相容性廣,動態功能大致上也還很好用。Native AOT 則不以執行階段 JIT 本身為前提,散發產物以原生執行檔為主,啟動相當容易改善,代價是限制變強。即使名稱裡都帶著 AOT,ReadyToRun 走的是「讓 JIT 輕鬆一點」,Native AOT 走的是「不以 JIT 為前提」,兩者的認真程度差很多。
WPF 或 WinForms 的應用程式可以使用 Native AOT 嗎?
現階段最好相當謹慎看待。Windows 的 Native AOT 沒有 built-in COM,WPF 和 trimming 合不來,WinForms 又重度依賴 built-in COM marshalling,兩者都不適合當第一個 Native AOT 候選。如果需要 COM,維持 JIT,或是以 ComWrappers / source-generated COM 為前提重新設計,有些情況下方向會比較對。當作入口的話,主控台程式、worker、小型 Web API 會直接了當得多。
Native AOT 適合什麼樣的應用程式?
適合以啟動為主角的 CLI 工具、在容器裡大量部署的小型 API、worker 與背景服務、無伺服器或短命處理程序、插進原生應用程式裡的小型 .NET 元件,以及不想要求執行環境事先安裝 .NET 執行階段的場合。這些情境的共同點是邊界比較清楚,動態機制也比較容易減少。反過來說,以執行階段載入外掛為主角的應用程式,或是重度依賴以反射尋找型別的框架的結構,就不適合。

作者檔案

本文作者的個人檔案頁面。

Go Komura

小村軟體有限公司 代表

以 Windows 軟體開發、技術諮詢與故障調查為中心,在難以重現的故障調查與既有資產仍在運作的專案上具有優勢。

回到部落格一覽