dotnet 使用 Crossgen2 對 DLL 進行 ReadyToRun 提升啟動效能

lindexi發表於2022-06-20

我對幾個應用進行嚴格的啟動效能評估,對比了在 .NET Framework 和 dotnet 6 下的應用啟動效能,非常符合預期的可以看到,在使用者的裝置上,經過了 NGen 之後的 .NET Framework 可以提供非常優越的啟動效能,再加上 .NET Framework 本身就是屬於系統元件的部分,很少存在冷啟動的時候,大部分的 DLL 都在系統裡預熱。啟動效能方面,依然是 .NET Framework 比 dotnet 6 快非常多。而在破壞了 .NET Framework 的執行時框架層的 NGen 之後,可以發現 .NET Framework 的啟動效能就比不過 dotnet 6 的啟動效能。為了在 dotnet 6 下追平和 .NET Framework 的啟動效能差異,引入與 NGen 的同等級的 ReadyToRun 用來提升整體的效能。本文將告訴大家如何在 dotnet 6 的應用裡面,使用 Crossgen2 工具,給 DLL 生成 AOT 資料,提升應用啟動效能

我預計本文是具有時效的,各個概念都在變更,本文是在 2022.05 編寫的。如果你閱讀本文的時間距離本文編寫時間過長,那請小心本文過期的知識誤導

開始之前,還請理清一下概念

在 dotnet 裡面,這些概念都在變來變去,還沒有完全定下來。在聊 dotnet 裡面的 AOT 之前,是必須先來做一個闢謠的。第一個謠言是 AOT 意味著效能更高? 其實不然,採用 AOT 能減少應用啟動過程中,從 IL 轉換為本機程式碼的損耗,但通過分層編譯(TieredCompilation)技術,這部分的差異不會特別特別大,再加上 dotnet 6 引入 的 QuickJit 技術,還能進一步縮小差距。但即使這麼說,啟動效能方面,採用 AOT 還是很有優勢的,因為啟動過程是效能敏感的,再加上大型專案在啟動過程中將需要執行大量的程式碼邏輯,即使 JIT 再快和加上動態 PGO 的輔助下,依然由於需要工作的量太多而在效能上不如採用 AOT 的方式。由於 AOT 是生產靜態邏輯,只取平臺最小集,而無法和 JIT 一樣,根據所執行裝置進行動態優化,這就是為什麼執行過程中的效能,在 JIT 進入 Tier 2 優化之後的效能要遠遠超過 AOT 的方式。換句話說,全程都使用 AOT 而不加入任何 JIT 只是提升啟動效能,但是降低了執行過程的效能

那如果我啟動效能也要,執行過程的效能也要呢?這個就是 ReadyToRun 技術的概念了,在 DLL 的進入呼叫時,先採用 AOT 技術,將部分邏輯預先跑了 JIT 且將跑了之後的二進位制邏輯也記錄到 DLL 裡面。如此可以實現在首次呼叫方法時,減少 JIT 的戲份,儘可能使用之前 AOT 的內容,從而提升應用啟動效能。而在應用跑起來之後,依然跑的是 JIT 的優化,如此即可兼顧啟動效能和執行過程的效能

如何實現 ReadyToRun 這個概念?就需要用到幾項技術和工具,其中 Crossgen2 就是進行 ReadyToRun 的工具。通過 Crossgen2 工具,可以對 DLL 進行靜態 AOT 編入 DLL 內

但是如此做法也不是沒有缺點的,那就是額外編入 DLL 的 AOT 的內容,將會增大 DLL 的體積。而 DLL 體積的增大將會降低啟動過程中讀取檔案的效能,再加上 AOT 和 JIT 過程的切換也是需要判斷邏輯,加上了這部分損耗之後,再對比一下 QuickJit 技術,實際上採用 Crossgen2 進行 ReadyToRun 不是對所有的 DLL 都能提升啟動效能

為了解決以上問題,在 dotnet 裡再引入了 PGO 的概念。啟動過程裡面呼叫的方法是有限的,如果可以瞭解到應用啟動過程將會呼叫哪些方法,只是將這部分方法進行 AOT 那麼對 DLL 體積的影響將會小非常多。這就是 PGO 需要解決的問題,通過引入 PGO 這個概念,在應用執行過程裡面,瞭解應用啟動過程將會碰到哪些 IL 邏輯,將這部分邏輯記錄下來,用於指導 ReadyToRun 過程進行 AOT 哪些方法。從而讓 AOT 過程不需要針對所有的 IL 邏輯,而是僅對應用啟動過程需要用到的才進行 AOT 過程。如此即可更大的提升應用的啟動效能。不過 PGO 可以做的事情可不只是 ReadyToRun 的指導,還可以作為 JIT 過程中,讓 JIT 瞭解可以預先在後臺執行緒裡面跑哪些 IL 轉換從而達到更高的啟動效能。必須說明的是,我詢問了幾位大佬瞭解到,當前的 PGO 還是一個玩具,雖然效能評測上可以達到很好的效果,然而還沒有具備釋出環境使用的能力

對於 AOT 不可反編譯的闢謠。如上文可以看到 ReadyToRun 技術上,依然是保留 IL 邏輯,只是在 DLL 裡面再加入 AOT 生成的二進位制資料,從而減少啟動過程的 JIT 的損耗。也就是說如果採用 ReadyToRun 的技術,可以讓應用有更快(不一定是更快)的啟動效能,同時也擁有原本的執行過程的效能。但是否可以做到不可反編譯,自然是做不到的,原本的 IL 程式碼依然還在,也就是說採用 ReadyToRun 技術,沒有任何額外的保護能力。那第二個問題,如果採用純 AOT 技術,能否達到程式碼保護能力?嗯,能加一點點。如果配合上混淆的話,感覺上是差不多了。如果要說防破解能力的話,兩個的打分,一個是 60 分,一個是 70 分,滿分是 100 分。真要別人看不懂,程式碼寫垃圾些就好了,我全力發揮的時候,保證連自己都看不懂

回到主題,如何在 dotnet 裡面通過 Crossgen2 工具進行 ReadyToRun 提升應用效能? 千萬別被官方騙了,如果只是在 csproj 上或者是在釋出的時候加上 ReadyToRun 的命令引數,恭喜你,是真的用了 Corssgen2 工具。但優化呢?只是優化了入口程式集而已

真的想要有比較大的優化,是需要將除了入口程式集之外的其他程式集也通過 Crossgen2 工具進行 ReadyToRun 才可以有比較大的提升的。例如我的一個大型應用,在啟動過程裡面將 WPF 框架裡面大概十分之一的模組都碰了一次,使用 JitInfo.GetCompiledMethodCount 瞭解到,在第一個視窗 Show 出來之前就有 5 萬個方法呼叫。這個應用的入口程式集佔比太小了,如果使用官方的方法,只是對入口程式集進行 ReadyToRun 那麼效能上還真被 .NET Framework 完虐

為了讓 dotnet 6 應用的啟動效能能媲美 .NET Framework 應用的啟動效能,可以採用 ReadyToRun 對標 .NET Framework 的 NGen 技術。以下將告訴大家如何使用 Crossgen2 工具對 DLL 進行 ReadyToRun 提升啟動效能

預設的 Crossgen2 工具是採用 NuGet 分發的 DotnetPlatform 型別的 NuGet 包,裡面包含了獨立釋出的 Crossgen2 工具。換句話說,可以在 %localappdata%\..\..\.nuget\packages\microsoft.netcore.app.crossgen2.win-x64 找到此工具。如果沒有找到的話,那試試用一句 dotnet publish -c Release -r win-x64 -p:PublishReadyToRun=true 命令讓 dotnet 為了構建 ReadyToRun 而幫你將 Crossgen2 下載

以上的 Crossgen2 工具放在 microsoft.netcore.app.crossgen2.win-x64 資料夾裡面,這裡的 win-x64 指的不是 Crossgen2 工具的能力,不是說這個資料夾的工具只能構建出 win-x64 的。而是說這個工具本身是 win-x64 的。這個工具是能構建出其他的平臺的 AOT 的。換句話說是在 Windows 的 32 位系統裡面,將會拉的工具是 microsoft.netcore.app.crossgen2.win-x86 的包

進入版本號資料夾,再進入 Tools 資料夾即可找到 Crossgen2.exe 可執行檔案,這就是工具本文。例如在我的裝置上的工具路徑是

C:\Users\lindexi\.nuget\packages\microsoft.netcore.app.crossgen2.win-x64\6.0.5\tools\Crossgen2.exe

接下來將告訴大家如何使用這個工具

這個工具的使用需要傳入的引數推薦是一個 rsp 檔案,大概的命令列呼叫如下

C:\Users\lindexi\.nuget\packages\microsoft.netcore.app.crossgen2.win-x64\6.0.5\tools\Crossgen2.exe "@C:\lindexi\Fxx\F1.rsp"

具體的引數都放在 rsp 檔案裡面,大概內容如下

--targetos:windows
--targetarch:x86
--pdb
-O
-r:"C:\Program Files (x86)\dotnet\shared\Microsoft.NETCore.App\6.0.5\api-ms-win-core-console-l1-1-0.dll"
-r:"C:\Program Files (x86)\dotnet\shared\Microsoft.NETCore.App\6.0.5\api-ms-win-core-console-l1-2-0.dll"
-r:"C:\Program Files (x86)\dotnet\shared\Microsoft.NETCore.App\6.0.5\api-ms-win-core-datetime-l1-1-0.dll"
-r:"C:\Program Files (x86)\dotnet\shared\Microsoft.NETCore.App\6.0.5\api-ms-win-core-debug-l1-1-0.dll"
-r:"C:\Program Files (x86)\dotnet\shared\Microsoft.NETCore.App\6.0.5\api-ms-win-core-errorhandling-l1-1-0.dll"
-r:"C:\Program Files (x86)\dotnet\shared\Microsoft.NETCore.App\6.0.5\api-ms-win-core-fibers-l1-1-0.dll"
-r:"C:\Program Files (x86)\dotnet\shared\Microsoft.NETCore.App\6.0.5\api-ms-win-core-file-l1-1-0.dll"
-r:"C:\Program Files (x86)\dotnet\shared\Microsoft.NETCore.App\6.0.5\api-ms-win-core-file-l1-2-0.dll"
--out:"C:\Users\linde\AppData\Local\Temp\Crossgen2\Crossgen2\KokicakawheeyeeWhemhedawfelawnemhel.dll"
C:\lindexi\Code\empty\KokicakawheeyeeWhemhedawfelawnemhel\KokicakawheeyeeWhemhedawfelawnemhel\bin\release\net6.0-windows\win-x86\publish\KokicakawheeyeeWhemhedawfelawnemhel.dll

大概由以下幾個部分組成。每一行都是一個獨立的引數,分別內容如下

  • --targetos:windows: 準備執行的系統平臺。進行 ReadyToRun 將生成 AOT 程式碼,這是平臺強相關的,必須說明是哪個平臺
  • --targetarch:x86: 準備生成的對應平臺,是 x86 還是 x64 等
  • --pdb: 這是可選的,表示要生成 PDB 符號檔案。如不加上這一句將不生成 PDB 檔案。生成的 PDB 檔案是 ni.pdb 檔案,配合原本的 DLL 的 PDB 檔案即可方便進行除錯
  • -O: 這是可選的,表示需要進行優化。相當於 Release 版本。推薦預設都加上,否則將幾乎沒有優化效果,或者說只有反向優化效果
  • -r:"xxx.dll": 這裡將會重複很多行,一行一個程式集檔案的本地路徑。讓工具瞭解到有哪些引用可以去找到。工具在準備 AOT 過程,需要找到所引用的程式集。這些引數就是告訴工具對應的程式集放在哪。可以多加入很多程式集,因為只是給工具使用的參考引用,工具會根據自己的需求,去找到對應的程式集檔案。如果工具發現傳入的有多餘的,那將會自動忽略多餘的。推薦將整個 dotnet runtime 都加入,但是要注意加入的版本必須是和釋出的版本是一致的,否則啟動過程如果炸了,那就涼涼。如果應用是獨立釋出的,那就列出應用獨立釋出資料夾裡面的所有 DLL 檔案,不需要加上額外的執行時資料夾
  • --out:"xx.dll": 處理之後的輸出檔案路徑
  • xxxxx.dll 輸入程式集的路徑

構建出 rsp 檔案,作為引數,呼叫 Crossgen2 工具,即可完成對程式集的 ReadyToRun 過程。多個程式集就多次重複以上過程即可

必須畫重點的是,呼叫 Crossgen2 工具進行 ReadyToRun 是不一定能提升啟動效能的,這是一個需要測量的過程。每個 DLL 在呼叫了 Crossgen2 工具進行 ReadyToRun 是會修改檔案體積的,整個變更也是會影響啟動效能的。推薦在優化應用啟動效能,進行足夠的測量,方法如下

使用 Crossgen2 工具對每個 DLL 來一次,包括框架層的 DLL 也來一次。然後逐個 DLL 替換,測量應用啟動效能。如果發現某些 DLL 進行了 ReadyToRun 反而降低啟動效能,或者某些 DLL 加大的檔案體積對比啟動效能的優化來說不划算,那就不對這些 DLL 進行優化

以下是測試的對 dotnet runtime 底層和 WPF 框架的 DLL 進行 ReadyToRun 優化之後,對 walterlv 大佬的某個應用的啟動效能的影響,值得一提的是對於不同的應用,測試的資料將會存在很大的出入,核心原因在於不同的應用啟動過程將訪問的模組有所不同

這個資料是沒有多少參考價值的,因為對於不同的應用來說,以上的結果將會有變化。如果你想要採用 ReadyToRun 技術提升應用啟動效能,還請必須測量每個 DLL 在經過 ReadyToRun 對啟動效能的影響。如果你的時間充裕的話,還可以測量對多個 DLL 優化的組合對啟動效能的影響

我所在團隊的某個大型應用,在經過了 ReadyToRun 技術的優化,啟動效能提升百分之三十

但也必須說明的是,不是所有的應用使用 ReadyToRun 都能有優化啟動效能,例如我的一個小應用,只要採用了 ReadyToRun 技術,啟動效能基本上都是降低了。總的來說,採用 ReadyToRun 技術是需要進行效能測量的

參考文件

WPF dotnet 使用本機映像 native 優化 dotnet framework 二進位制檔案

WPF 通過 ReadyToRun 提升效能

Conversation about crossgen2 - .NET Blog

runtime/crossgen2-compilation-structure-enhancements.md at main · dotnet/runtime

runtime/Program.cs at main · dotnet/runtime

編譯配置設定 - .NET Microsoft Docs

ReadyToRun deployment overview - .NET Microsoft Docs

利用 PGO 提升 .NET 程式效能 - hez2010 - 部落格園

JitInfo.GetCompiledMethodCount(Boolean) Method (System.Runtime) Microsoft Docs

相關文章