始めに
dotnet-5.0におけるNativeAOTについて、どういうものかという概要と、バイナリを生成するところまで書きたいと思う。
AOT(Ahead Of Time)コンパイルとは
通常dotnetのアセンブリは、ソースからILと呼ばれる中間言語に変換された形でいったん出力され、実行時にコンパイラによって、ネイティブコードに変換されて処理が実行される。これをJIT(Just In Time)コンパイルという
この変換処理を予め行い、ネイティブコードに落とし込んだバイナリを生成するのがAOT(Ahead Of Time)コンパイルとなる。
特に新しい概念というわけではなく、C/C++やrust,golang等はAOTと言えると思う。
dotnetでAOTを導入する目的
そもそもdotnetではILからのJITが基本だが、それでもAOTが検討されるのは、以下の理由による。
起動時間を含む積極的な最適化
JITでは、実行時に最適化をかけるという仕組み上、起動時に著しく時間がかかるような最適化を行うことができない。
対してAOTでは、ビルド時に最適化をかけることが可能なため、時間的な制約からはある程度解放される(実用上限度はあるが)。
そのため、より深いコード解析を行うことにより、より積極的な最適化が可能になる。
また、JITという工程を飛ばすことにより、起動時間の短縮が可能となる。
起動時間の短縮というだけならば、ReadyToRunという既存のAOTで実現可能。ただし、後の最適化のためにILは温存されるので、生成物のサイズ自体は増える。
ただし、JITでしかできない最適化というものはあるので、最適化でどちらが良いかというのは一概に言えるものではないというのが難しい所ではある。
また、dotnet系言語は強力なリフレクションや動的コード生成を前提にした機能や手法などが数多く存在するが、AOTとは相性が悪いため、かなり制限を受けるというのもデメリットとはいえる。
サイズの削減
ビルド時に依存関係を解決することにより、不要なコードを積極的に行い、最終的な生成物のサイズを軽減できる。
大抵のアプリケーションは、ベースライブラリの全ての機能を使用しているわけではないため、この手法により、大幅なサイズ削減が見込める。
ただし、基本的にILよりもネイティブコードの方がサイズが大きくなりがちなのと、C#の基礎的な機能(例外とか)を実現するためのIL→ネイティブコードの変換量がかなり大きくなるようなプラットフォーム(iOSとかWASMとか)では、却って生成物のサイズが大きくなる場合もある。
また、リフレクションを多用するようなプロジェクトでは、リフレクション用のメタ情報を持たせる必要があるため、結局コード削減量よりメタ情報追加による増加の方が勝ってしまい、生成されるもののサイズは大きくなってしまう場合がある。
プラットフォームの制限
例えばiOS等は動的コードの実行を禁止しているため、JITがそもそも行えず、AOTを行うしかないという事情がある。
NativeAOTとは
ILを完全にネイティブコードに落とし込むプロジェクトで、以前はcorertと呼ばれていた。
実はCarrionというゲームでcorert+monogameの組み合わせで使われているらしいが、あくまでcorert自体は実験的プロジェクトで、実用する際は注意が必要なものだった。
それが、最近dotnet/runtimelabという、ランタイム自体に関する実験的修正を試みるプロジェクトに移って開発されることとなった。runtimelabに移ったことで、将来的にどのようにメインプロジェクトに統合されるかというのが具体的に視野に入るようになったので、前進はしていると思う。
現在の進捗を確認したい場合は、個別のブランチに移って履歴を確認することができる
NativeAOTの特徴
NativeAOTは、以下のような特徴を持つ
- 今のところサポートしてるのはWindows、Linux、macOS(x64)
-
corertの時はWASM等の生成も出来たようだが、runtimelabに移ってからは記述は見つからない
- ドキュメントに載ってないだけで使えるかもしれない
-
corertの時はWASM等の生成も出来たようだが、runtimelabに移ってからは記述は見つからない
- Cコンパイラが必要(C/C++のコードに一旦落としてる?)
-
ネイティブアプリだけでなく、ネイティブライブラリの生成も可能
- 関数のみで、かつ引数、戻り値等に制限は付く
- 動的なコード実行に強い制限がかかる
- 複雑なリフレクションを行おうとするとエラーが出る場合がある
- 構築時に予めメタ情報を持たせておくことで、ある程度可能
- 典型的なパターンの場合は自動的にビルド時に判断して持たせてくれる
- メタデータを持たせる場合は、サイズがそれなりに増大する
- 構築時に予めメタ情報を持たせておくことで、ある程度可能
- System.Reflection.Emit以下のものはできないと考えた方が良い
- 複雑なリフレクションを行おうとするとエラーが出る場合がある
- 生成バイナリがプラットフォーム依存となる
- これは単一実行バイナリと同じ
メリットデメリットがかなり激しいので、採用する場合はよく考えて採用した方が良い。
NativeAOTを体験する
現状これらの制約を踏まえた上でNativeAOTを行いたい場合、ソースからビルドするというのがまず推奨されるやり方だが、
そこまでするのはきついという人も多いと思うので、ビルド済みバイナリを利用して、nuget経由で比較的手軽にできる方法を紹介する。ただし、dotnetにおけるNativeAOTはあくまでも発展途上で、大胆な仕様変更や不可解なエラーも十分あり得るため、取り扱いには細心の注意を払う事
事前準備
事前に必要なものに関しては、 https://github.com/dotnet/runtimelab/blob/feature/NativeAOT/samples/prerequisites.md に記述がある。
上記に加えて、dotnet-sdk 5.0以降が必要。
また、一部のlinux,mac環境では、clangコンパイラが別名表記になっている場合がある(clang-6.0
とか後ろにバージョン表記が入る等)。
そのような環境でビルドする際は、msbuildのプロパティにCppCompilerAndLinker=[clangのパス]
を設定する必要がある。
プロジェクトの作成からバイナリの生成まで
-
dotnet new
等で、通常のアプリケーションプロジェクトを作成する- TargetFrameworkは"net5.0"以降、あるいは"netcoreapp*"にする
- csprojがあるディレクトリに、
dotnet new nugetconfig
等でnuget.configを作成する - nuget.configに
https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-experimental/nuget/v3/index.json
をソースとして追加する -
Microsoft.DotNet.ILCompiler
のバージョン6.0.0-*
をnuget参照に追加する -
dotnet publish -r [RID] -c [Configuration]
を実行する- RIDについては 公式ドキュメントを参考にすること
以上を実行すると、bin\[Configuration]\[TargetFramework]\[RID]\native
以下に実行可能バイナリが生成される。
現状特に設定を追加せずに行うと、HelloWorldレベルのもので大体7MB程度になった。
今回は詳しく検証していないが、ビルドオプションの工夫で、もっと減らせる余地もあるらしい。ビルドオプション等は以下。
https://github.com/dotnet/runtimelab/blob/feature/NativeAOT/docs/using-nativeaot/optimizing.md
今回は試してないが、ネイティブライブラリの作成も可能らしい。
終わりに
NativeAOTについて、とりあえず入り口となる部分を書いたが、今後も仕様変更等は当然起こり得る事なので、適度に動向を追っていきたい。
後、この手の技術については、dotnetだけではなく他でも色々研究されている分野なので、それらと比較していってもいいかもしれない。
参考リンク
-
dotnet/runtimelab
- dotnetランタイムに関わる実験場
-
dotnet/runtimelabのNativeAOT関連
- tools/aot,nativeaot等に変更点がある
- NativeAOTを利用する時のガイド等
- サンプルプロジェクト
-
ReadyToRun
- ReadyToRunについて
-
.NET Runtime Form Factors
- .NETランタイムについての議論2020年版
- NativeAOTや、他のAOTについても触れている
- corertの使用例
-
Carrion
- "いのちの輝きくんみたい"とか一部で言われてたゲーム
- ゴア描写でCERO D(17歳以上対象)なので、苦手な人は注意
-
Streets of Rage 4
- 日本だと"ベア・ナックル IV"という名前になっているゲーム
-
dotnet-compressor
- 圧縮、伸長ユーティリティ
- 手前味噌ながら
- corert使わないと確か100MB程度になっていたと思う
-
Carrion