LoginSignup
12
7

はじめに

これは、「祝 .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++によるデスクトップ開発」にチェックを入れ、「インストール」をクリックします。
image.png

ビルド

HelloWorldのサンプルが下記サイトにあります。
https://github.com/dotnet/runtimelab/tree/feature/NativeAOT/samples/HelloWorld

HelloWorldのままではつまらないので、今回はフィボナッチ数列にしてみます。

プロジェクト生成

自分はC:\WorkSpaceフォルダにいつもプロジェクトを生成しています。

> cd C:\WorkSpace
> dotnet new console -o Fibonacci
> cd Fibonacci

下図のファイルが生成されます。
image.png

プロジェクトにNativeAOTを追加

> dotnet new nugetconfig
テンプレート "NuGet Config" が正常に作成されました。

nuget.configファイルが追加されます。
image.png

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" />

最終結果

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="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が追加されました。

Fibonacci.csproj
<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のままなのでプログラムを書き換えます。

Program.cs
// See https://aka.ms/new-console-template for more information
Console.WriteLine("Hello, World!");

フィボナッチ数列にプログラムを書き換えます。
C# 10.0 からは、namespace、Main、using Systemを記述しなくてスクリプトライクに記述できます。
参照:最初の C# プログラム(.NET 6 新テンプレート)

Program.cs
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\

image.png

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要素に幾つかオプションを追加してみます。
今回は実行速度より小さいコードサイズを優先にします。

Fibonacci.csproj
<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

image.png

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

image.png

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)は遅いというレッテルが、いつか解消される時がくるかもしれません。

12
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
12
7