🙆‍♀️

C# WebAssembly AOT vs Chrome Javascriptでベンチマークしてみた

2022/12/21に公開

C# .NET WebAssembly vs Chrome Javascript

.NET7になりBlazorも正式にAOTコンパイルができるようになりました.
今後は.NET WebAssembly without Blazor UIにある通り,Blazorなしの純粋なC#でもWebAssemblyがサポートされます.
ますます.NETのWebAssemblyが進化していきますね.

AOTコンパイルすることでJSとどの程度パフォーマンスに差があるのか,WebAssemblyでC#や.NETを採用するメリットがあるのか検証してみました.

前提条件

厳密なベンチマークではなく簡単なものです.
また,コードの最適化は行ってません.
C#のものをできるだけJSで再現してますが,
JS側でUint8Arrayではなくただの2次元配列を利用していたり,
まだまだ最適化できる部分はあると思いますが,
大まかなベンチマークなので目安だと思ってください.

アルゴリズムは,
以前書いた記事と同じマンデルブロー集合を生成するコードを利用します.
https://zenn.dev/remrem/articles/b9e5c9bde22e15

結果

検証マシン

  • Memory DDR4 32GB 2666Mhz
  • CPU Core i7 12700
  • Chromium 108
Platform 実行速度
Javascript 116ms
.NET Blazor AOT 68ms
.NET 7 Blazor 2226ms
.NET 7 44ms

結論

今回の計測では意外にもBlazor AOTのほうがJavascriptよりも高速でした.
今後はパフォーマンス目的でC#や.NETを採用するメリットになりそうですね!

計測に利用したコード

今回はいろいろごにょごにょしたので,
計測部分のみ載せておきます.

C#
record struct Size(int Width, int Height);

class Program
    public static void Run() {
        var size = new Size(1000, 1000);
        var buffer = new byte[size.Width, size.Height];

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

        _ = GenerateMandelbrot(buffer, size, -2.0, -1.6, 1.0, 1.6, 100);

        sw.Stop();
        Console.WriteLine(sw.ElapsedMilliseconds);

        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;
        }
    }
}

Javascript
const size = { width: 1000, height: 1000 };
const buffer = (new Array(1000)).fill((new Array(1000)).fill(0));

const startTime = Date.now();

GenerateMandelbrot(buffer, size, -2.0, -1.6, 1.0, 1.6, 100);

const endTime = Date.now();
console.log(endTime - startTime);

function GenerateMandelbrot(
    buffer,
    size,
    xMin,
    yMin,
    xMax,
    yMax,
    iterLimit
) {
    for (let py = 0; py < size.height; py++) {
        for (let px = 0; px < size.width; px++) {
            // get coordinates of pixel (px, py)
            const x = xMin + ((xMax - xMin) * px) / (size.width - 1);
            const y = yMin + ((yMax - yMin) * py) / (size.height - 1);

            let a = x, b = y;
            let iter = 0;
            do {
                // use tuples for iteration
                a = a * a - b * b + x
                b = 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
                const color = (iter % 2 == 0 ? 1 : 2);
                buffer[py, px] = color;
            }
        }
    }

    return buffer;
}

Discussion