🏃‍♂️

.NET 7 NativeAOT でCPU主体の処理のベンチマークをしてみた

2022/09/06に公開

初めに

.NET 7も現在preview 7となり、RC1も間近となりました。

毎年恒例の中の人によるパフォーマンスに関する長~い記事も来ました。

https://devblogs.microsoft.com/dotnet/performance_improvements_in_net_7/

.NET 7よりJITコンパイルだけでなく、GoやRust、C++などと同じくAOTコンパイルができるようになります。
AOTとJITの違いについて詳しくはググってみてください。

今回はCPUを主体とする処理において、パフォーマンスがどうなのか簡易的ではありますが、検証してみたいと思います。
Benchmark.NETがNET7にまだ対応していないので標準クラスライブラリのStopwatchで計測を行います。

検証内容はマンデルブロ集合を生成するプログラムで実験を行います。
マンデルブロ集合については今回は説明を省略します。

環境セットアップ

.NET 7がインストールされていれば、csprojファイルにオプションを追加するだけみたいです。

<PropertyGroup>
	<OutputType>Exe</OutputType>
	<TargetFramework>net7.0</TargetFramework>
	<ImplicitUsings>enable</ImplicitUsings>
	<Nullable>enable</Nullable>

	<PublishAot>true</PublishAot>
	<SelfContained>true</SelfContained>
	<IlcOptimizationPreference>Speed</IlcOptimizationPreference>
	<IlcOptimizationPreference>Size</IlcOptimizationPreference>
	<IlcFoldIdenticalMethodBodies>true</IlcFoldIdenticalMethodBodies>
</PropertyGroup>

詳しくは公式ドキュメントをみてください。
https://docs.microsoft.com/ja-jp/dotnet/core/deploying/native-aot/

公式ドキュメントにはまだ記載がありませんがパフォーマンスに関して、以下のオプションが有効みたいなので追加しました。
SpeedかSizeが指定できます。
C++のコンパイルオプションみたいに、速度優先か、バイナリサイズの小ささ優先化が選べるみたいです。

<IlcOptimizationPreference>Speed</IlcOptimizationPreference>
or
<IlcOptimizationPreference>Size</IlcOptimizationPreference>

参考
https://www.nikouusitalo.com/blog/net-7-self-contained-nativeaot-and-readytorun-cheatsheet/

Build

以下のコマンドでビルドができます。

dotnet publish -r win-x64 -c Release

想定通りIlcOptimizationPreferenceのオプションによりバイナリサイズに違いがありました。

IlcOptimizationPreference Size
Size 3,455 kb
Speed 3,561 kb

ソース

まずはマンデルブロ集合がうまく生成されることを確認します。
60 x 60の画像が生成される以下のプログラムを実行してみます。

var size = new Size(60, 60);
var buffer = new byte[size.Width, size.Height];
var m = GenerateMandelbrot(buffer, size, -2.0, -1.6, 1.0, 1.6, 100);
for (int y = 0; y < size.Height; y++) {
    for (int x = 0; x < size.Width; x++) {
        Console.Write(m[y, x] switch {
            0 => ".",
            1 => "#",
            _ => "+",
        });
    }
    Console.WriteLine();
}

static byte[,] GenerateMandelbrot(
        byte[,] buffer,
        Size size,
        double xMin,
        double yMin,
        double xMax,
        double yMax,
        int iterLimit
    ) {
    for (int py = 0; py < size.Height; py++) {
        for (int px = 0; px < size.Width; px++) {
            // get coordinates of pixel (px, py)
            double x = xMin + ((xMax - xMin) * px) / (size.Width - 1);
            double y = yMin + ((yMax - yMin) * py) / (size.Height - 1);

            double a = x, b = y;
            int iter = 0;
            do {
                // use tuples for iteration
                (a, b) = (a * a - b * b + x, 2 * a * b + y);
                iter++;
            } while (iter <= iterLimit && a * a + b * b < 4);

            if (iter > iterLimit) {
                // interior color
                buffer[py, px] = 0;
            }
            else {
                // exterior color banded by iteration
                byte color = (byte)(iter % 2 == 0 ? 1 : 2);
                buffer[py, px] = color;
            }
        }
    }

    return buffer;
}

コンソールに60 x 60 のマンデルブロ集合画像が出力され、うまくいっていることが確認できました。

++++++++++++++++++########################++++++++++++++++++
++++++++++++++++##################+++++#####++++++++++++++++
++++++++++++++################++++#####+++####++++++++++++++
++++++++++++################+++++##++++#++++####++++++++++++
+++++++++++################+++++###++#+##++++####+++++++++++
+++++++++################+++++++##+#####+#++++#####+++++++++
++++++++################+++++++##+###+#+###++++#####++++++++
+++++++###############+++++++###+#+##.###+###+++#####+++++++
++++++###############+++++++##++#++#...+##+###+++#####++++++
+++++###############++++++##+++####....##+#+++##+######+++++
+++++##############+++++##+##+#####+...#++#+##+##+#####+++++
++++#############++++####++####+.........#+++#+##+######++++
+++#############+++#####++##+...............+.##+#+######+++
+++###########++#######+++++##..............+.+++#+######+++
++##########++#++####+++#+.++.................#++#++######++
++#######+++#++#########+##....................++#++######++
++####+++++##+++#+++#+#+++......................#+++######++
+###++++++###++#+#+#+#+#+#.....................++++++######+
+##+++++####+#+++.#..#+#+......................+#++++######+
+#+++++########++.......#......................#+#+++######+
++++++##+++++##+........#......................++#+++######+
++#++#+++###+##................................++#+++######+
+#++##+#+#+++.................................##+#+++######+
+#++##+#+#+++.................................##+#+++######+
++#++#+++###+##................................++#+++######+
++++++##+++++##+........#......................++#+++######+
+#+++++########++.......#......................#+#+++######+
+##+++++####+#+++.#..#+#+......................+#++++######+
+###++++++###++#+#+#+#+#+#.....................++++++######+
++####+++++##+++#+++#+#+++......................#+++######++
++#######+++#++#########+##....................++#++######++
++##########++#++####+++#+.++.................#++#++######++
+++###########++#######+++++##..............+.+++#+######+++
+++#############+++#####++##+...............+.##+#+######+++
++++#############++++####++####+.........#+++#+##+######++++
+++++##############+++++##+##+#####+...#++#+##+##+#####+++++
+++++###############++++++##+++####....##+#+++##+######+++++
++++++###############+++++++##++#++#...+##+###+++#####++++++
+++++++###############+++++++###+#+##.###+###+++#####+++++++
++++++++################+++++++##+###+#+###++++#####++++++++
+++++++++################+++++++##+#####+#++++#####+++++++++
+++++++++++################+++++###++#+##++++####+++++++++++
++++++++++++################+++++##++++#++++####++++++++++++
++++++++++++++################++++#####+++####++++++++++++++
++++++++++++++++##################+++++#####++++++++++++++++
++++++++++++++++++########################++++++++++++++++++
++++++++++++++++++++++################++++++++++++++++++++++
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

ベンチマーク

このままでは、速すぎて正確な計測ができないかもしれないので、
サイズを60 x 60 から3000 x 3000に変更します。

var size = new Size(60, 60);

さらに、10回処理を実行して、経過時間の平均を求めて出力します。

var sw = new Stopwatch();
sw.Start();

for (int i = 0; i < 10; i++) {
    var m = GenerateMandelbrot(buffer, size, -2.0, -1.6, 1.0, 1.6, 100);
}

sw.Stop();

Console.WriteLine(sw.ElapsedMilliseconds / 10);

結果

検証マシン

  • Memory DDR4 32GB 2666Mhz
  • CPU Core i7 12700

せっかくなのでいろいろなプラットフォームでも行って見ました。

Platform 実行速度
.NET 5 367ms
.NET 6 364ms
.NET 7 363ms
.NET 7 NativeAOT 357ms

NativeAOTのほうが若干速いという結果になりました。
今後はNativeAOTでなきゃできないような最適化がかかるようになって更にRustやC++に近づくうれしいですね。

今回は処理が短いのでIlcOptimizationPreferenceによる実行速度の違いはありませんでした。

IlcOptimizationPreference 実行速度
Size 357ms
Speed 357ms

結論

TL;DR: .NET 7 is fast. Really fast. 😎

(おまけ)計測に使用したソース


using System;
using System.Diagnostics;
using System.Drawing;

var size = new Size(3000, 3000);
var buffer = new byte[size.Width, size.Height];

var sw = new Stopwatch();
sw.Start();

for (int i = 0; i < 10; i++) {
    var m = GenerateMandelbrot(buffer, size, -2.0, -1.6, 1.0, 1.6, 100);
}

sw.Stop();

Console.WriteLine(sw.ElapsedMilliseconds / 10);

static byte[,] GenerateMandelbrot(
        byte[,] buffer,
        Size size,
        double xMin,
        double yMin,
        double xMax,
        double yMax,
        int iterLimit
    ) {
    for (int py = 0; py < size.Height; py++) {
        for (int px = 0; px < size.Width; px++) {
            // get coordinates of pixel (px, py)
            double x = xMin + ((xMax - xMin) * px) / (size.Width - 1);
            double y = yMin + ((yMax - yMin) * py) / (size.Height - 1);

            double a = x, b = y;
            int iter = 0;
            do {
                // use tuples for iteration
                (a, b) = (a * a - b * b + x, 2 * a * b + y);
                iter++;
            } while (iter <= iterLimit && a * a + b * b < 4);

            if (iter > iterLimit) {
                // interior color
                buffer[py, px] = 0;
            }
            else {
                // exterior color banded by iteration
                byte color = (byte)(iter % 2 == 0 ? 1 : 2);
                buffer[py, px] = color;
            }
        }
    }

    return buffer;
}

Discussion