【C#】コンパイルが必要な言語はダメらしいので「NativeAOT」について紹介
NativeAOTって?
- 最近のC#/.NETは結構早い
- gRPCベンチマークだとマルチコアでトップになることも
- 「C#は遅くて~」というのは古い情報。情報のアップデートを!
-
早い理由の一つは NativeAOT
- ※他には
Span<T>
やSIMD対応、DynamicPGOとか色々
- ※他には
-
NativeAOTって?
- ネイティブバイナリ向けに事前(Ahead-Of-Time)コンパイルする仕組み
- 「C#はJITコンパイルで~」というのは古い情報。情報のアップデートを!
.NETの3つの種類のAOTコンパイル
R2R
- AOTとJITのハイブリッド方式
- 起動速度が速くなる
- サイズは大きくなる
- クロスコンパイルできる [1]
MonoAOT
- モバイル(iOS/Android)とWeb(WASM)で使われてる方式
- 名前の通り、mono由来
- 「monoが消えた・無くなった」と誤解してる人がいますが、ちゃんと使われてマス
monoと言えば…
「Microsoftが「Mono」をWineチームに寄贈、Microsoftの手を離れることでMonoが再び活気を取り戻すと期待する声も - GIGAZINE」というニュースがありましたが、
Microsoftの手を離れたMono
というのは誤報で、単に(もう更新がほとんどされなくなった)「mono/mono」のリポジトリの管理がWineに移管されるだけ、とのことです。
mono自体の開発のメインストリームは「dotnet/runtime/mono」にあり、引き続き.NET SDK/ランタイムの中でmonoの開発は進行していくそうです。
Microsoft maintains a modern fork of Mono runtime in the dotnet/runtime repo and has been progressively moving workloads to that fork. That work is now complete, and we recommend that active Mono users and maintainers of Mono-based app frameworks migrate to .NET which includes work from this fork.
https://github.com/mono/mono/issues/21796
NativeAOT
- Win/macOS/Linuxで使えるAOT
- モバイル・WASMはexperimental
- .NET 7.0 SDKから正式に使えるように
- 徐々に制限が少なくなったり、機能が増えたりしている
参考:Avalonia UIでNativeAOTを使ったモバイル・WASM向け比較実験
いいところ・わるいところ
-
いいところ
- 起動時間が早い
- そのまま動く
- サイズも比較的小さい
- ネイティブライブラリも作れる
- 難読化になってる
-
悪いところ
- モバイル・WASMはまだ試験対応
- クロスコンパイルできない
- 使えないコードがある
- ライブラリが対応してないことがまだ多い
- コンパイルが通っても動かないことがある
👍 いいところ
- 起動時間が早い
- JITコンパイルが無いので素早く起動します
- .NETアプリの「もっさり起動」がありません!
- JITコンパイルが無いので素早く起動します
- そのまま動く
- .NETランタイム不要
- 対象プラットホームでそのまま動くアプリになります
- サイズも比較的小さい
- ランタイムが入る関係でデフォでTrimming処理が入ります
- NativeAOTでなくとも.NETランタイム同梱できますが、サイズを小さくするには追加の設定が必要
- ネイティブライブラリも作れる
- 難読化になってる
- フツーに作ると.NETアプリは簡単にソースが再現できちゃう
- NativeAOTはそこまで簡単じゃなくなる
👎 わるいところ
- モバイル・WASMはまだ試験対応
- まだまだ使える状態ではないようです
- クロスコンパイルできない
- NativeAOTは基本的に対象プラットホーム上でコンパイルする必要がある
- R2Rはクロスコンパイルできる
- さらにVisualStudioやXCodeが必要
- クロスコンパイル - .NET | Microsoft Learn
- NativeAOTは基本的に対象プラットホーム上でコンパイルする必要がある
- 使えないコードがある
- 動的に呼び出してる系のコードは割とアウト
- Limitations of Native AOT deployment
- ライブラリが対応してないことがまだ多い
- 判定アナライザーあるけど.NET8から
- 上の「使えないコード」を使ってると対応してない
- コンパイルが通っても動かないことがある
- Trimmingされるので必要な処理までTrimされちゃうことがある
- コンパイルエラーにならない…
- 動かしてみるまで分からないのがツライ…
- どこに問題あるのかわからないのでツライ…
- デバッグ方法が通常と違うのでツライ…
NativeAOTで何が変わる?
-
.NETアプリの配布方法の選択肢が増える
- ガチガチに最適化して最速省サイズを狙うのがNativeAOT
- 昔ながらのお気軽配布もマダマダ選択肢
-
ソースジェネレータの重要度が高まっている
- NativeAOTでは動的処理(リフレクション等)はSource Generatorベースの処理に置き換える必要がある
-
GeneratedRegex
やSystem.Text.Json
などどんどんSGベースの処理が追加されてる
-
定番ライブラリの世代交代が起きてる
- 非対応の定番ライブラリからNativeAOT Readyな新しいライブラリに置き換わりつつある
- 定番でも開発が止まってるライブラリは今後廃れていくかも
- 例:
- Json.NET → System.Text.Json
- Prism → CommunityToolkit.MVVM
- CSVHelper → Sep
- 非対応の定番ライブラリからNativeAOT Readyな新しいライブラリに置き換わりつつある
NativeAOT対応って具体的にどうする?
開発環境
- .NET SDK
- 最新のver
- 正式対応は.NET 7.0からだけどもっと基本新しい方がオススメ
- 最新のver
- Windows
- VisualStudio
- Desktop development with C++ workload
- ※インストールしてあれば VSCode とかでもビルド可能
- macOS
- Command Line Tools for XCode
- Ubuntu
- clang
- zlib1g-dev
- libicu-dev
sudo apt-get install dotnet-sdk-8.0 libicu-dev cmake zlib1g-dev -y
ビルド設定(csproj)
<!-- 必須 -->
<PublishAot>true</PublishAot>
<!-- NativeAOT対応と宣言する -->
<IsAotCompatible>true</IsAotCompatible>
<!-- 判定だけ有効にするなら -->
<EnableTrimAnalyzer>true</EnableTrimAnalyzer>
<!-- 速度とサイズどちらを優先にするか(Size/Speed) -->
<OptimizationPreference>Size</OptimizationPreference>
<!-- 追加のCPU限定命令を有効に出来るオプション -->
<IlcInstructionSet>native</IlcInstructionSet>
<IlcMaxVectorTBitWidth>512</IlcMaxVectorTBitWidth>
<!-- 次のTrim設定は自動で有効になるので指定不要 -->
<!-- <PublishTrimmed>true</PublishTrimmed> -->
<!-- <EnableTrimAnalyzer>true</EnableTrimAnalyzer> -->
<!-- 同時に有効になるTrimmingの設定(full/partial) -->
<TrimMode>partial</TrimMode>
<!-- .NET8.0以降はこの設定は指定不可 -->
<!-- <TrimMode>copyused</TrimMode> -->
<!-- サイズを小さくする追加設定(以下2つは常に有効でもOK) -->
<InvariantGlobalization>true</InvariantGlobalization>
<StripSymbols>true</StripSymbols>
<!-- Trimming警告有効 -->
<SuppressTrimAnalysisWarnings>false</SuppressTrimAnalysisWarnings>
<!-- 監視・テレメトリ系ライブラリを使うときはtrue -->
<EventSourceSupport>true</EventSourceSupport>
<!-- フレームワークによっては必須設定 -->
<!-- 例:Avalonia UI は BuiltInComInteropSupportをtrueにする必要あり -->
<BuiltInComInteropSupport>true</BuiltInComInteropSupport>
<!-- 例:ASP.NET Core -->
<EnableConfigurationBindingGenerator>true</EnableConfigurationBindingGenerator>
-
PublishAot=true
で有効 -
OptimizationPreference
で速度orサイズの優先を変えられる - CPU限定命令を追加で有効に出来るオプションがある
-
PublishTrimmed
もNativeAOTで自動で有効になる -
TrimMode
はデフォはfull
- なのでtrimingし過ぎで動かない、が良く起きる
-
partial
指定して、ちゃんとアセンブリを一つ一つ指定してあげる必要あり
- Trimmingのオプションはたくさんある
- フレームワーク ライブラリの機能をトリミングする
- フレームワークやライブラリによってはこの追加設定が必須
<ItemGroup>
<!-- TrimMode=partialの時の指定 -->
<TrimmableAssembly Include="【使ってるDLL(アセンブリ)】" />
<!-- ルートアセンブリの指定 -->
<TrimmerRootAssembly Include="MyAssembly" />
<!-- xmlで書いて指定することもできる -->
<TrimmerRootDescriptor Include="./path/to/trd.xml" />
</ItemGroup>
TrimmableAssembly
で指定すると、その指定されたものが対象になります。
TrimmerRootAssembly
で指定すると、そこから繋がってるコードを参照してくれるそうです。
"ルート" アセンブリとは、トリマーがライブラリ内のすべての呼び出しを分析し、そのアセンブリを起点とするすべてのコード パスを横断することを意味します。
TrimmerRootDescriptor
でxmlで指定すると、メソッドとかプロパティの単位で指定できます。
動的に呼び出してるような場合は、この方法が必要なよう?です。
<linker>
<assembly fullname="MyAssembly">
<type fullname="MyAssembly.MyClass">
<method name="DynamicallyAccessedMethod" />
</type>
</assembly>
</linker>
参考:Rd.xmlについて
パブリッシュ
dotnet publish -c Release -r <runtime_id_here>
- runtime id(RID)の指定は必須
dotnet publish -c Release -r win-x64
ソースコード側の対応(C#)
警告対応が必要
- ILxxx っていう警告がでまくる
-
[RequiresUnreferencedCode]
/[RequiresDynamicCode]
属性- NativeAOTに互換性がないところ(メソッド等)に付ける属性
- 怒られなくなるだけで使えるようになるわけじゃないので注意
-
[UnconditionalSuppressMessage]
属性- NativeAOT非対応だけど、呼び出しても問題ない時に付ける属性
- 処理が分岐されてて何も起きない、とか
- .NET8.0以前の古いTargetFrameworkに対応するためにPolySharpとかのpolyfillを入れる
トリミング警告対応も必要
- トリミング用に .NET ライブラリを準備する - .NET | Microsoft Learn
-
[DynamicallyAccessedMembers]
属性 -
[UnconditionalSuppressMessage]
属性 -
[DynamicDependency]
属性 - .NET8.0以前の古いTargetFrameworkに対応するためにPolySharpとかのpolyfillを入れる
NativeAOT対応ライブラリを使う
- CommunityToolkit.MVVM - ver 8.3で対応
- ConsoleAppFramework - ver 5で対応
- Dapper -> Dapper.AOT
NativeAOT対応の処理にソースコードを書き換える
- json処理はSystem.Text.Jsonのソースジェネレータベースの処理に書き換える
- リフレクションは使わないか、SGベースのライブラリを使う処理にする
- 正規表現は
GeneratedRegex
にする- Codefixで自動修正可能
[GeneratedComInterface]
- 動的なdll読込は避ける
-
[DllImport]
->[LibraryImport]
-
- .NET9.0では
Microsoft.Extensions.DependencyInjection
も対応が良くなるらしい
NativeAOT対応アシストライブラリ・ツールも使う
- PublishAotCompressed
- CompiledExpressions: Create Compiled Delegates from Expressions with nested member support and binding accessibility checking. AOT compatible with no run-time reflection.
- ascpixi/dotnet-ilc: 🛠 A .NET tool wrapper to manually run ilc - the IL compiler.
- WinFormsComInterop: ComWrappers required to run NativeAOT and WinForms
- SourceGear.Bridge.NativeAOT.Rust
- PublishAotCross: NuGet package to help you cross-compile Native AOT to different OSes/architectures
デバッグできるようにする
-
ふつうの.NETアプリのデバッグだと、NativeAOT対応で動かない理由はわからない
-
WinDbg や gdb をつかってネイティブデバッグする必要がある
-
csprojに追加の設定を加えるとNativeAOTのデバッグが少し色々できるようになる
<PropertyGroup>
<!-- シングルスレッドでコンパイルを実行 -->
<IlcSingleThreaded>true</IlcSingleThreaded>
<!-- メタデータ ログの生成 -->
<IlcGenerateMetadataLog>true</IlcGenerateMetadataLog>
<!-- オブジェクトのレイアウトを記述するログ ファイルを生成 -->
<IlcGenerateMapFile>true</IlcGenerateMapFile>
<!-- IL生成のdumpを生成 -->
<IlcDumpGeneratedIL>true</IlcDumpGeneratedIL>
<!-- サイズ情報を含む mstat形式ファイルの生成 -->
<IlcGenerateMstatFile>true</IlcGenerateMstatFile>
<!-- DGML 形式のログの生成 -->
<IlcGenerateDgmlFile>true</IlcGenerateDgmlFile>
</PropertyGroup>
- 上の情報を元に解析ツールなどでさらに調べられる
-
sizoscopeX :
IlcGenerateMstatFile
/IlcGenerateDgmlFile
のデータからサイズ削減の調査ができるようになる
-
sizoscopeX :
実際のところどうなの?
-
まだちょっと早いかも
- ライブラリ側が対応できてない事が多くてどうしようもないことがある
- 対応ライブラリだけでやれば結構イケる
- モバイル・WASMだとまだ有効にならない
- 「何で動かないのかわからない問題」が結構ツライ
- ライブラリ側が対応できてない事が多くてどうしようもないことがある
-
「書いたコードが大体どこでも動く」っていうメリットをつぶしちゃう
- そのメリットをつぶしてまでやることなの?は考えたほうがいい!
-
他の選択肢で十分なこともけっこうある
- 起動を早くするだけならR2R
- サイズを小さくするだけならPublishTrimmed=true
- アプリ単体で動くだけならPublishSingleFile=true
- .NETアプリの配布方法はいっぱいあって、そのうちの一つ
- 全部がNativeAOTになるわけじゃない!!
-
NativeAOT対応の半分くらいはTrimming対策
- なので途中でNativeAOT対応諦めてもサイズ小さくすることはできるメリットもある
-
モバイル対応が来てから本番?
- monoAOTはAndroidで問題あり
- ゲームエンジンのUnityがついにCoreCLR(ふつうの.NETのやつ)に移行しそうなのでNativeAOT活用が本格化しそう
参考
- 公式MS learn
- Using Native AOT
- How to make libraries compatible with native AOT - .NET Blog
-
ascpixi/smolsharp: 🐜 Extremely small standalone C# executables using NativeAOT
- いろんな裏技を駆使してめちゃくちゃ小さいサイズを実現するサンプル
- bflattened/bflat: C# as you know it but with Go-inspired tooling (small, selfcontained, and native executables)
- .NET Native AOT Explained - NDepend Blog
- AOT Support? · microsoft/CsWin32 · Discussion #1169
- AOT with Reflection · dotnet/runtime · Discussion #95244
-
mac/LinuxからWindows向けはできません ↩︎
Discussion