🔥

【C#】コンパイルが必要な言語はダメらしいので「NativeAOT」について紹介

2024/09/14に公開

NativeAOTって?

  • 最近のC#/.NETは結構早い
    • gRPCベンチマークだとマルチコアでトップになることも
    • 「C#は遅くて~」というのは古い情報。情報のアップデートを!

https://x.com/Dave_DotNet/status/1673393475507867649

  • 早い理由の一つは NativeAOT

    • ※他にはSpan<T>やSIMD対応、DynamicPGOとか色々
  • NativeAOTって?

    • ネイティブバイナリ向けに事前(Ahead-Of-Time)コンパイルする仕組み
    • 「C#はJITコンパイルで~」というのは古い情報。情報のアップデートを!

.NETの3つの種類のAOTコンパイル

R2R

https://learn.microsoft.com/ja-jp/dotnet/core/deploying/ready-to-run

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はまだ試験対応
    • クロスコンパイルできない
    • 使えないコードがある
    • ライブラリが対応してないことがまだ多い
    • コンパイルが通っても動かないことがある

👍 いいところ

👎 わるいところ

  • モバイル・WASMはまだ試験対応
    • まだまだ使える状態ではないようです
  • クロスコンパイルできない
  • 使えないコードがある
  • ライブラリが対応してないことがまだ多い
    • 判定アナライザーあるけど.NET8から
    • 上の「使えないコード」を使ってると対応してない
  • コンパイルが通っても動かないことがある
    • Trimmingされるので必要な処理までTrimされちゃうことがある
    • コンパイルエラーにならない…
      • 動かしてみるまで分からないのがツライ…
      • どこに問題あるのかわからないのでツライ…
      • デバッグ方法が通常と違うのでツライ…

NativeAOTで何が変わる?

  • .NETアプリの配布方法の選択肢が増える
    • ガチガチに最適化して最速省サイズを狙うのがNativeAOT
    • 昔ながらのお気軽配布もマダマダ選択肢
  • ソースジェネレータの重要度が高まっている
    • NativeAOTでは動的処理(リフレクション等)はSource Generatorベースの処理に置き換える必要がある
    • GeneratedRegexSystem.Text.JsonなどどんどんSGベースの処理が追加されてる
  • 定番ライブラリの世代交代が起きてる
    • 非対応の定番ライブラリからNativeAOT Readyな新しいライブラリに置き換わりつつある
      • 定番でも開発が止まってるライブラリは今後廃れていくかも
    • 例:
      • Json.NET → System.Text.Json
      • Prism → CommunityToolkit.MVVM
      • CSVHelper → Sep

NativeAOT対応って具体的にどうする?

開発環境

  • .NET SDK
    • 最新のver
      • 正式対応は.NET 7.0からだけどもっと基本新しい方がオススメ
  • 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)

csproj PropertyGroup
<!-- 必須 -->
<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>
csproj
<ItemGroup>
  <!-- TrimMode=partialの時の指定 -->
  <TrimmableAssembly Include="【使ってるDLL(アセンブリ)】" />

  <!-- ルートアセンブリの指定 -->
  <TrimmerRootAssembly Include="MyAssembly" />

  <!-- xmlで書いて指定することもできる -->
  <TrimmerRootDescriptor Include="./path/to/trd.xml" />
</ItemGroup>

TrimmableAssemblyで指定すると、その指定されたものが対象になります。
TrimmerRootAssemblyで指定すると、そこから繋がってるコードを参照してくれるそうです。

"ルート" アセンブリとは、トリマーがライブラリ内のすべての呼び出しを分析し、そのアセンブリを起点とするすべてのコード パスを横断することを意味します。

TrimmerRootDescriptorでxmlで指定すると、メソッドとかプロパティの単位で指定できます。
動的に呼び出してるような場合は、この方法が必要なよう?です。

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>
Windows(x64)向け
dotnet publish -c Release -r win-x64

ソースコード側の対応(C#)

警告対応が必要

  • ILxxx っていう警告がでまくる
  • [RequiresUnreferencedCode] / [RequiresDynamicCode]属性
    • NativeAOTに互換性がないところ(メソッド等)に付ける属性
    • 怒られなくなるだけで使えるようになるわけじゃないので注意
  • [UnconditionalSuppressMessage]属性
    • NativeAOT非対応だけど、呼び出しても問題ない時に付ける属性
    • 処理が分岐されてて何も起きない、とか
  • .NET8.0以前の古いTargetFrameworkに対応するためにPolySharpとかのpolyfillを入れる

トリミング警告対応も必要

NativeAOT対応ライブラリを使う

NativeAOT対応の処理にソースコードを書き換える

https://learn.microsoft.com/ja-jp/dotnet/core/deploying/trimming/incompatibilities

  • json処理はSystem.Text.Jsonのソースジェネレータベースの処理に書き換える
  • リフレクションは使わないか、SGベースのライブラリを使う処理にする
  • 正規表現はGeneratedRegexにする
    • Codefixで自動修正可能
  • [GeneratedComInterface]
  • 動的なdll読込は避ける
    • [DllImport] -> [LibraryImport]
  • .NET9.0ではMicrosoft.Extensions.DependencyInjectionも対応が良くなるらしい

NativeAOT対応アシストライブラリ・ツールも使う

デバッグできるようにする

  • ふつうの.NETアプリのデバッグだと、NativeAOT対応で動かない理由はわからない

  • WinDbg や gdb をつかってネイティブデバッグする必要がある

  • csprojに追加の設定を加えるとNativeAOTのデバッグが少し色々できるようになる

csproj
<PropertyGroup>
  <!-- シングルスレッドでコンパイルを実行 -->
  <IlcSingleThreaded>true</IlcSingleThreaded>
  <!-- メタデータ ログの生成 -->
  <IlcGenerateMetadataLog>true</IlcGenerateMetadataLog>
  <!-- オブジェクトのレイアウトを記述するログ ファイルを生成 -->
  <IlcGenerateMapFile>true</IlcGenerateMapFile>
  <!-- IL生成のdumpを生成 -->
  <IlcDumpGeneratedIL>true</IlcDumpGeneratedIL>

  <!-- サイズ情報を含む mstat形式ファイルの生成 -->
  <IlcGenerateMstatFile>true</IlcGenerateMstatFile>
  <!-- DGML 形式のログの生成 -->
  <IlcGenerateDgmlFile>true</IlcGenerateDgmlFile>
</PropertyGroup>
  • 上の情報を元に解析ツールなどでさらに調べられる
    • sizoscopeXIlcGenerateMstatFile/IlcGenerateDgmlFileのデータからサイズ削減の調査ができるようになる

実際のところどうなの?

  • まだちょっと早いかも
    • ライブラリ側が対応できてない事が多くてどうしようもないことがある
      • 対応ライブラリだけでやれば結構イケる
    • モバイル・WASMだとまだ有効にならない
    • 「何で動かないのかわからない問題」が結構ツライ
  • 「書いたコードが大体どこでも動く」っていうメリットをつぶしちゃう
    • そのメリットをつぶしてまでやることなの?は考えたほうがいい!
  • 他の選択肢で十分なこともけっこうある
    • 起動を早くするだけならR2R
    • サイズを小さくするだけならPublishTrimmed=true
    • アプリ単体で動くだけならPublishSingleFile=true
    • .NETアプリの配布方法はいっぱいあって、そのうちの一つ
      • 全部がNativeAOTになるわけじゃない!!
  • NativeAOT対応の半分くらいはTrimming対策
    • なので途中でNativeAOT対応諦めてもサイズ小さくすることはできるメリットもある
  • モバイル対応が来てから本番?

参考

脚注
  1. mac/LinuxからWindows向けはできません ↩︎

Discussion