C# / .NET 7 でlinuxのネイティブコード出力(NativeAOT)

毎年恒例 Stephen Toub さんの **[Performance Improvements in .NET 7](https://devblogs.microsoft.com/dotnet/performance_improvements_in_net_7/)** が8月末に投稿されました。

https://devblogs.microsoft.com/dotnet/performance_improvements_in_net_7/
  
毎年.NETの改善状況を見るのを楽しみにしているのですが、今回はC#erならだれもが夢見る、C#でネイティブコード出力する機能、 **NativeAOT** がとても気になります。  
記事ではWindowsの例が書いてありますが **linuxで試してみたい** と思います。

## Native AoTとは

Native AOTはCPUネイティブコード（機械語）を直接出力する機能です。  
@yajuさんの「[dotnet-6.0におけるNativeAOTについて](https://qiita.com/yaju/items/d5a771f3f00672aa19ff)」がとても詳しいのでそちらを是非みてください。

ネイティブ化によって

- パフォーマンス向上
- バイナリーサイズの縮小
- dotnet未インストールマシンでの実行

などの利点が見込めます。例えばラズパイや小さな組み込みマシンでdotnetランタイムをインストールしなくともプログラムが動くとなればかなりのメリットになります。  

現在はコンソールアプリのみ等の[制約](https://github.com/dotnet/runtime/blob/main/src/coreclr/nativeaot/docs/limitations.md)があり、まだまだ改善されていく機能だと思いますが今後の発展がとても楽しみです。


## dotnet7.0-preview7 のインストール

ここは本題ではないので簡単に。
今回はRHEL9互換の **AlmaLinux 9** を最小インストールした状態から始めます。最小だとtarコマンドすらないのでそこからです。

```bash
$ sudo dnf install tar gzip libicu
$ curl -O https://download.visualstudio.microsoft.com/download/pr/aabf15d3-f201-4a6c-9a7e-def050d054af/0a8eba2d8abcf1c28605744f3a48252f/dotnet-sdk-7.0.100-preview.7.22377.5-linux-x64.tar.gz
$ mkdir -p $HOME/dotnet && tar zxf dotnet-sdk-7.0.100-preview.7.22377.5-linux-x64.tar.gz -C $HOME/dotnet
$ export DOTNET_ROOT=$HOME/dotnet
$ export PATH=$PATH:$HOME/dotnet
$ dotnet --version
7.0.100-preview.7.22377.5
```
exportの2行は `.bashrc` にでも書いておきましょう。

## Hello, World

パフォーマンスを見たかったのですが今回は出力バイナリを見ることにします。「Hello,World」でいきます。

```
$ dotnet new console -o con1
```
```csharp:con1/Program.cs
Console.WriteLine("Hello, World!");
```

## ILコード出力

まずは普通にIL版。シングルファイルにしてみます。

```bash:単一ファイル
$ dotnet publish --os linux -c release -o ~/bin/ -p:PublishSingleFile=true
$ ls -l ~/bin
-rwxr-xr-x.  186058  con1
```
ライブラリ依存で**186KB**。

```bash:ランタイム内包
$ dotnet publish --os linux -c release -o ~/bin/ \
    --self-contained=true \
    -p:PublishSingleFile=true \
    -p:PublishReadyToRun=true \
    -p:PublishTrimmed=true
$ ls -l ~/bin
-rwxr-xr-x. 1 16697946  con1
```
dotnetランタイムをすべて内包した場合は**16MB**になりました。  
Hello,World で 16MB は大きいですね・・。

## Native AOTを使えるようにする

ここからが本番。記事を読む限りcsprojファイルに `<PublishAot>true</PublishAot>` を追記することでコンパイル出来るように見えます。

```xml:cat con1.csproj
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net7.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
    <PublishAot>true</PublishAot>
  </PropertyGroup>
</Project>
```

これで`dotnet publish` を実行してみると以下のようにエラーが出ます。

```bash
error : Platform linker ('clang' or 'gcc') not found in PATH. Try installing appropriate package for clang or gcc to resolve the problem. [/home/xxx/con1/con1.csproj]
```

ということで足りないツール、ライブラリを追加でインストールします。  
必要なものは`gcc`, `g++`, `zlib`です。

```bash
$ sudo dnf install gcc gcc-c++ zlib-devel
```

Ubuntuなら `sudo apt-get install clang zlib1g-dev` でしょうか([公式情報](https://learn.microsoft.com/ja-jp/dotnet/core/deploying/native-aot/))。
これでネイティブコンパイルできるようになります。出来上がったバイナリーサイズを見てみます。

```bash
$ dotnet publish -r linux-x64 -c release -o ~/bin2/
$ ls ~/bin2/
-rwxr-xr-x. 1  15609816  con1
```

**15MB**。ちょっと大きいですね。

## バイナリーを小さくする。

[Performance Improvements in .NET 7](https://devblogs.microsoft.com/dotnet/performance_improvements_in_net_7/) ではWindowsのバイナリーは3MBなのでもっと小さくなる余地はあるはずです。この記事にある以下のコンパイルオプションを適用して再度確認してみます。

- `InvariantGlobalization = true` カルチャー情報を無視
- `UseSystemResourceKeys = true` 例外メッセージを除去
- `IlcGenerateStackTraceData = false` スタックトレース情報を最低限に
- `DebuggerSupport = false` デバッガサポートなし

その他今回は関係なさそうなSerializerやHttp関連も含めて記述してみました。

```xml:con1.csprojに追記
    <InvariantGlobalization>true</InvariantGlobalization>
    <UseSystemResourceKeys>true</UseSystemResourceKeys>
    <IlcOptimizationPreference>Size</IlcOptimizationPreference>
    <IlcGenerateStackTraceData>false</IlcGenerateStackTraceData>
    <DebuggerSupport>false</DebuggerSupport>
    <EnableUnsafeBinaryFormatterSerialization>false</EnableUnsafeBinaryFormatterSerialization>
    <EventSourceSupport>false</EventSourceSupport>
    <HttpActivityPropagationSupport>false</HttpActivityPropagationSupport>
    <MetadataUpdaterSupport>false</MetadataUpdaterSupport>
```

**11MB** まで削減できました。

```bash
$ dotnet publish -r linux-x64 -c release -o ~/bin3/
$ ls -l ~/bin3/con1
-rwxr-xr-x. 11199792  con1
```



## refrection 関連を除去

今回のコードではrefrectionは使っていないので [このドキュメント](https://github.com/dotnet/corert/blob/master/Documentation/using-corert/optimizing-corert.md)を参考にしてrefrection関連のコードを落とします。

```xml:csprojへの追記
<RootAllApplicationAssemblies>false</RootAllApplicationAssemblies>
<IlcGenerateCompleteTypeMetadata>false</IlcGenerateCompleteTypeMetadata>
<IlcDisableReflection>true</IlcDisableReflection>
<IlcOptimizationPreference>Size</IlcOptimizationPreference>
<IlcFoldIdenticalMethodBodies>true</IlcFoldIdenticalMethodBodies>
```

これで**6MB**まで減りました。

```
$ ls -l ~/bin4/
-rwxr-xr-x. 1 6233888  con1
```

## 埋め込みデバッグ情報を除去

バイナリーを覗いてみるとデバッグ用文字列が入っています。

```bash
$ strings ~/bin4/con1
...
S_P_CoreLib_System_Buffers_SpanAction_2<Char__S_P_CoreLib_System_ValueTuple_3<IntPtr__Int32__S_P_CoreLib_System_HexConverter_Casing>>
S_P_CoreLib_System_ValueTuple_2<System___Canon__Int32>
S_P_CoreLib_System_Buffers_SpanAction_2<Char__S_P_CoreLib_System_ValueTuple_2<System___Canon__Int32>>
S_P_CoreLib_System_ValueTuple_3<System___Canon__System___Canon__Int32>
S_P_CoreLib_System_Buffers_SpanAction_2<Char__S_P_CoreLib_System_ValueTuple_3<System___Canon__System___Canon__Int32>>
...
```

[公式ドキュメント](https://github.com/dotnet/corert/blob/master/Documentation/using-corert/optimizing-corert.md)でも「MacやLinuxではデバッグ情報がデフォルトで入るのでstripで削ってね」と書いてあるので除去してみます。

```
$ cp con1 con2
$ strip con2
$ ls -l
-rwxr-xr-x. 6233888  con1
-rwxr-xr-x. 1428120  con2
```

**1.4MB** まで減りました。

**2022年9月10日追記**

linux/mac用にcsprojのオプション `<StripSymbols>true</StripSymbols>` がありました。これを使うとstripコマンドと同じ効果があり、デバッグシンボルを *.dbgとして別ファイルに出力してくれます。

## 出力バイナリーの確認と考察

出来上がったバイナリーは以下の依存関係を持っています。

```c
$ ldd ~/bin3/con1
    linux-vdso.so.1        // linuxシステムコール用ライブラリ
    libstdc++.so.6         // GNU C++ 標準ライブラリ
    libm.so.6              // 数学系の標準ライブラリ
    libc.so.6              // Linux Cライブラリ
    ld-linux-x86-64.so.2   // ELF形式ファイルのローダー
    libgcc_s.so.1          // gccのライブラリ
```

どんなコードが出力されているのかまでは追ってません。

コンパイルの様子を見るとcppやcファイルは出力されず、DLLが一度出力されています。  
ILコードを何らかの形でネイティブにしていると思われますが、Unityの[IL2CPP](https://docs.unity3d.com/ja/2019.4/Manual/IL2CPP.html)のようにcppにするのではなく、JITのコンパイル結果と同じようなものなのかもしれません。その辺はいつか逆アセンブリして確かめてみたいです。

また、「Hello,World」が **1.4MB** は納得できない人が多いかもしれません。  
が、Console.WriteLine()はStream 実装が残るでしょうし、文字列を使うため `string` の実装が、さらにGC（ガベージコレクション）も入っているのでしょう。まずはしょうがないのかなと思います。

## 参考

公式ドキュメント(GitHub)
https://github.com/dotnet/runtime/tree/main/src/coreclr/nativeaot/docs

Optimizing programs targeting Native AOT  
https://github.com/dotnet/runtime/blob/main/src/coreclr/nativeaot/docs/optimizing.md