はじめに
これは、「祝 .NET 6 GA!.NET 6 での開発 Tips や試してみたことなど、あなたの「いち推し」ポイントを教えてください【PR】日本マイクロソフト Advent Calendar 2021」の9日目の記事となります。
NativeAOTに興味を持ったのは、Delphi 5で作成されていたインターフェイス用のコンソールアプリケーションがあり、C# に作り変えたのですが、速度でDelphi(ネイティブコード)に勝てないんですよね。
C#版でもネイティブコードになれば Delphi版に近づけるかも知れない、そんな淡い期待をNativeAOTにしてましたよ。うらぎれらたけど・・・
.NETネイティブ
.NETネイティブ(Native) とは、.NET を対象とするアプリケーションは、特定のプログラミング言語で記述され、中間言語(IL)にコンパイルされます。この中間言語(IL)を完全にコンパイルして直接ネイティブコードを生成する技術となります。
実行時に中間言語(IL)からコンパイルするJIT(Just-In-Time)と違い、実行前にコンパイルするので事前コンパイラAOT(Ahead-Of-Time)と呼ばれます。
.NET の生産性の高さと、ネイティブコードのハイパフォーマンスを入手できることができる一挙両得の技術として注目されています。
NativeAOTとは
NativeAOTで検索すると日本語だと下記サイトのみくらい情報が薄いです。今回タイトルも真似てみました。
ILを完全にネイティブコードに落とし込むオープンソースプロジェクトです。ILからの変換なので、C#以外にVisual BasicやF#でも可能です。
2021年以降は、NativeAOTリポジトリ
https://github.com/dotnet/runtimelab/tree/feature/NativeAOT
※将来的にはメインプロジェクトに統合される予定
2020年以前は、corertリポジトリ(CoreRT)
https://github.com/dotnet/corert
※corert自体は実験的プロジェクトで、実用する際は注意が必要なものだった。
注意点として、このテクノロジーは、リフレクションにあまり依存できないため少し制限があります。
NativeAOTコンパイルでは、最初にRyuJITを使用してILをLLVM IRにコンパイルします。このプロセスは、IL固有のパターンに関連するコードを最適化します。次に、LLVM IRをネイティブバイナリプログラムにコンパイルします。このプロセスは、LLVMによってさらに最適化され、コンパイル済みになります。ボリュームが小さくなり、実行時のパフォーマンスが向上します。
https://www.cnblogs.com/hez2010/p/dotnet-with-native-aot.html
参照:.NET CoreのアプリケーションをCoreRTを利用してビルドする方法
Visual Basicでも試してみました。
NativeAOTの準備
C++ ビルドツール
マイクロソフト C++ ビルドツールのインストール
https://visualstudio.microsoft.com/ja/visual-cpp-build-tools/
C++ によるデスクトップ開発
Visual Studio Installerを起動します。変更ボタンをクリックします。
ワークロードタブに「C++によるデスクトップ開発」にチェックを入れ、「インストール」をクリックします。
ビルド
HelloWorldのサンプルが下記サイトにあります。
https://github.com/dotnet/runtimelab/tree/feature/NativeAOT/samples/HelloWorld
HelloWorldのままではつまらないので、今回はフィボナッチ数列にしてみます。
プロジェクト生成
自分はC:\WorkSpaceフォルダにいつもプロジェクトを生成しています。
> cd C:\WorkSpace
> dotnet new console -o Fibonacci
> cd Fibonacci
プロジェクトにNativeAOTを追加
> dotnet new nugetconfig
テンプレート "NuGet Config" が正常に作成されました。
nuget.configの編集
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
<!--To inherit the global NuGet package sources remove the <clear/> line below -->
<clear />
<add key="nuget" value="https://api.nuget.org/v3/index.json" />
</packageSources>
</configuration>
packageSources要素のより下を下記の置き換えます。
<add key="dotnet-experimental" value="https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-experimental/nuget/v3/index.json" />
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" protocolVersion="3" />
最終結果
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
<!--To inherit the global NuGet package sources remove the <clear/> line below -->
<clear />
<add key="dotnet-experimental" value="https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-experimental/nuget/v3/index.json" />
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" protocolVersion="3" />
</packageSources>
</configuration>
ILCompilerの追加
> dotnet add package Microsoft.DotNet.ILCompiler -v 7.0.0-*
Fibonacci.csproj にItemGroup要素にMicrosoft.DotNet.ILCompiler
が追加されました。
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.DotNet.ILCompiler" Version="7.0.0-*" />
</ItemGroup>
</Project>
プログラムの修正
HelloWorldのままなのでプログラムを書き換えます。
// See https://aka.ms/new-console-template for more information
Console.WriteLine("Hello, World!");
フィボナッチ数列にプログラムを書き換えます。
C# 10.0 からは、namespace、Main、using Systemを記述しなくてスクリプトライクに記述できます。
参照:最初の C# プログラム(.NET 6 新テンプレート)
Fibonacci_Iterative(10);
void Fibonacci_Iterative(int len)
{
int a = 0, b = 1, c = 0;
Console.Write("{0} {1}", a,b);
for (int i = 2; i < len; i++)
{
c = a + b;
Console.Write(" {0}", c);
a = b;
b = c;
}
}
NativeAOTによる発行
下記は発行のコマンドの説明になります。
> dotnet publish -r <RID> -c <Configuration>
RIDには、win-x64
or linux-x64
or osx-x64
を指定します。
残念ながらlinux-arm64
がまだありませんので、Raspberry Pi 4用には出来ない。
Configurationには、 debug
or release
を指定します。
では、win-x64のreleaseで発行します。
> dotnet publish -r win-x64 -c release
Fibonacci -> C:\WorkSpace\FibonacciVB\bin\release\net6.0\win-x64\publish\
Fibonacci.exe でサイズが 4.15MiB あります。自分はWindows Terminal上でPowershellを使用しているので、先頭に"./"を付けて実行します。
> cd bin\release\net6.0\win-x64\native
> ./Fibonacci.exe
0 1 1 2 3 5 8 13 21 34
Linux-Arm および Linux-Arm64 サポート対象外
2021/12/09現在、ラズベリーパイ用にNativeAOTでコンパイルしたが、「error : Cross-OS native compilation is not supported. https://github.com/dotnet/corert/issues/5458」なります。
いずれは対応してくれることを願います。
dotnet publish -r linux-arm64 -c release
or
dotnet publish -r linux-arm -c release
イメージがネイティブか判断する
対象のexeが、.NET(managed)で作られたか判断するには、dumpbin /clrheader
を使用して確認する。
dumpbinにはmspdb80.dll
が必用で、”C:\Program Files (x86)\Microsoft Visual Studio 9.0\Common7\IDE“などにある。
参照:イメージがネイティブであるか CLR であるかを確認する
cd "C:\Program Files (x86)\Microsoft Visual Studio 9.0\VC\bin"
dumpbin /clrheader [ファイルパス]
.NET(managed)で作られていた場合、File Type:に続いてclr Header:
の項目が表示されます。
ネイティブの場合、下記のようにSummary
の項目が表示されます。
参照:インサイド .NET Framework [改訂版] 第5回 アセンブリのロードとセキュリティ
File Type: EXECUTABLE IMAGE
Summary
35000 .data
194000 .managed
28000 .pdata
1C1000 .rdata
14000 .reloc
1000 .rsrc
77000 .text
1000 _RDATA
イメージサイズ縮小
現在、Fibonacci.exe でサイズが 4.15MiB あります。
PropertyGroup要素に幾つかオプションを追加してみます。
今回は実行速度より小さいコードサイズを優先にします。
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<!-- https://github.com/dotnet/corert/blob/master/Documentation/using-corert/optimizing-corert.md -->
<IlcOptimizationPreference>Size</IlcOptimizationPreference>
<RootAllApplicationAssemblies>false</RootAllApplicationAssemblies>
<IlcGenerateCompleteTypeMetadata>false</IlcGenerateCompleteTypeMetadata>
<IlcGenerateStackTraceData>false</IlcGenerateStackTraceData>
<IlcDisableReflection>true</IlcDisableReflection>
<IlcFoldIdenticalMethodBodies>true</IlcFoldIdenticalMethodBodies>
<IlcDisableUnhandledExceptionExperience>false</IlcDisableUnhandledExceptionExperience>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.DotNet.ILCompiler" Version="7.0.0-*" />
</ItemGroup>
</Project>
IlcOptimizationPreference
Size:最適化されたコードを生成するときは、小さいコードサイズを優先します。
Speed:最適化されたコードを生成するときは、コードの実行速度を優先します。
RootAllApplicationAssemblies
デフォルト:true
一部のリフレクションコーディングの使用法との互換性のために、コンパイラが未使用のコードを削除しないようにします。このオプションをfalseに設定すると、実行可能ファイルのサイズが小さくなります。
IlcGenerateCompleteTypeMetadata
デフォルト:true
実行可能ファイルからオブジェクトにアクセスする場合に、メタデータタイプの生成を許可します。このオプションをfalseに設定すると、実行可能ファイルのサイズが小さくなります。
IlcGenerateStackTraceData
デフォルト:true
スタックトレースでのテキスト名の生成を許可します。このオプションをfalseに設定すると、実行可能ファイルのサイズが小さくなります。
IlcDisableReflection
デフォルト:false
リフレクションフリーモードは、CoreRTコンパイラとランタイムのモードであり、リフレクションAPIの機能を大幅に削減
https://github.com/dotnet/corert/blob/master/Documentation/using-corert/reflection-free-mode.md
IlcFoldIdenticalMethodBodies
デフォルト:false
このオプションは、同一のメソッド本体を折りたたむ(メソッド本体の重複排除)。このオプションをtrueに設定すると、実行可能ファイルのサイズが小さくなります。しかし、予期しない動作が発生する可能性があります。
IlcDisableUnhandledExceptionExperience
未処理の例外のスタックトレースをコンソールに出力するコードを無効にします。
IlcInvariantGlobalization
デフォルト:false
英語以外のカルチャをサポートするコードとデータを削除するグローバリゼーション不変モードを有効にします。コードとデータを削除すると、アプリが小さくなります。
https://github.com/dotnet/corert/blob/master/Documentation/using-corert/optimizing-corert.md
再発行
> cd C:\WorkSpace\Fibonacci
> dotnet publish -r win-x64 -c release
Fibonacci.exe のサイズが 4.15 MiB → 1.13 MiB まで小さくなりました。
実行結果も問題ありません。
> cd bin\release\net6.0\win-x64\native
> ./Fibonacci.exe
0 1 1 2 3 5 8 13 21 34
注意
先頭に記載したインターフェイスプログラム(TestIF.exe)ですが、21.9 MB(23,029,760 バイト)あるので、サイズを縮小する幾つかのオプションを追加した結果 8.81 MB (9,247,232 バイト)と減らすことが出来た。
ただし、最適化前の速度が平均 570ms に対して、最適化後の速度が平均 1660ms と約3倍遅くなった。
サイズをただ小さくすればいいというものではないようです。
究極に小さく
下記サイトではあらゆるテクニックを駆使して、64MiBから8176バイトまで小さくしています。
Building a self-contained game in C# under 8 kilobytes
bflatの紹介
単純なアプリケーションの場合は bflat を使用すると良い結果が得られる可能性があります。
How to compile a .NET application to native code? - stackoverflow
bflatは、.NET実行可能ファイルを生成する「公式」C#コンパイラである。最も単純なアプリであっても、サイズが2MBから3MBの実行可能ファイルを生成します。
https://github.com/MichalStrehovsky/bflat
その他
Redditにて、NativeAOTの.NET 7での計画のスレッドがあります。
NativeAOT .NET 7 Plans
Is Native AOT on the roadmap?
WinFormsとNativeAOTについて、.NET 5の時なので.NET 6で対応されているかも。
Again WinForms and NativeAOT
最後に
NativeAOTは、まだ発展途上のプロジェクトになります。何れVisual Studioに統合されると思います。
C#の組みやすさでネイティブコードが生成できて速度は速くなるという世界がいずれ来ます。
.NET(managed)は遅いというレッテルが、いつか解消される時がくるかもしれません。