WriteableBitmap塗りつぶしベンチマーク(Safe / Parallel / Unsafe / Unsafe+Parallel)

コンピュータ

WPFの画像オブジェクトのWriteableBitmapの塗りつぶしルーチンを自前で作成しました。
UnsafeとParallel.Forの組み合わせで高速化するか確認してみたいと思います。

ソースコード

ファイル名:WirteBitmapBench.csproj

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0-windows</TargetFramework>
    <UseWPF>true</UseWPF>
    <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
  </PropertyGroup>

</Project>

Unsafeを許可する為にcsprojで以下の設定を行っています。
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>

ファイル名:Program.cs

using System;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Imaging;

internal class Program
{
    [STAThread]
    private static void Main()
    {
        int width = 8000;
        int height = 8000;

        var tests = new (string name, Action<WriteableBitmap> action)[]
        {
            ("Safe", FillGradientSafe),
            ("SafeParallel", FillGradientSafeParallel),
            ("Unsafe", FillGradientUnsafe),
            ("UnsafeParallel", FillGradientUnsafeParallel),
        };

        // ★ 10回平均
        int N = 10;

        Console.WriteLine($"Resolution = {width} x {height}");
        Console.WriteLine($"Run Count = {N}");

        var rng = new Random();

        foreach (var t in tests)
        {
            Console.WriteLine($"--- Warm-up: {t.name} ---");

            // ★ ウォームアップ(結果捨て)
            var warmupWb = CreateBitmap(width, height);
            t.action(warmupWb);

            // ★ GCクリーンアップ(安定化)
            GC.Collect();
            GC.WaitForPendingFinalizers();
            GC.Collect();
        }

        var results = tests.Select(test =>
        {
            long total = 0;

            for (int i = 0; i < N; i++)
            {
                // ★ ランダム順序にしたい場合はここでシャッフル(省略可)

                var wb = CreateBitmap(width, height);
                var sw = Stopwatch.StartNew();
                test.action(wb);
                sw.Stop();

                total += sw.ElapsedMilliseconds;

                // ★ GCクリーンアップ
                GC.Collect();
                GC.WaitForPendingFinalizers();
                GC.Collect();
            }

            return (test.name, avg: total / (double)N);
        });

        Console.WriteLine();
        Console.WriteLine("=== Result (Average) ===");

        foreach (var r in results)
        {
            Console.WriteLine($"{r.name,-16} : {r.avg:F2} ms");
        }
    }

    private static WriteableBitmap CreateBitmap(int w, int h) =>
        new WriteableBitmap(w, h, 96, 96, PixelFormats.Bgra32, null);

    // ---- SAFE ----
    private static void FillGradientSafe(WriteableBitmap wb)
    {
        int width = wb.PixelWidth;
        int height = wb.PixelHeight;
        int stride = wb.BackBufferStride;
        int bytes = stride * height;

        byte[] buffer = new byte[bytes];
        wb.CopyPixels(buffer, stride, 0);

        for (int y = 0; y < height; y++)
        {
            int row = y * stride;
            byte gy = (byte)(y & 0xFF);

            for (int x = 0; x < width; x++)
            {
                int idx = row + x * 4;
                buffer[idx + 0] = (byte)(x & 0xFF);
                buffer[idx + 1] = gy;
                buffer[idx + 2] = (byte)((x + y) & 0xFF);
                buffer[idx + 3] = 255;
            }
        }

        wb.WritePixels(new Int32Rect(0, 0, width, height),
                       buffer, stride, 0);
    }

    // ---- SAFE + PARALLEL ----
    private static void FillGradientSafeParallel(WriteableBitmap wb)
    {
        int width = wb.PixelWidth;
        int height = wb.PixelHeight;
        int stride = wb.BackBufferStride;
        int bytes = stride * height;

        byte[] buffer = new byte[bytes];
        wb.CopyPixels(buffer, stride, 0);

        Parallel.For(0, height, y =>
        {
            int row = y * stride;
            byte gy = (byte)(y & 0xFF);

            for (int x = 0; x < width; x++)
            {
                int idx = row + x * 4;
                buffer[idx + 0] = (byte)(x & 0xFF);
                buffer[idx + 1] = gy;
                buffer[idx + 2] = (byte)((x + y) & 0xFF);
                buffer[idx + 3] = 255;
            }
        });

        wb.WritePixels(new Int32Rect(0, 0, width, height),
                       buffer, stride, 0);
    }

    // ---- UNSAFE ----
    private static unsafe void FillGradientUnsafe(WriteableBitmap wb)
    {
        wb.Lock();
        try
        {
            int width = wb.PixelWidth;
            int height = wb.PixelHeight;
            int stride = wb.BackBufferStride;

            byte* basePtr = (byte*)wb.BackBuffer;

            for (int y = 0; y < height; y++)
            {
                byte* row = basePtr + y * stride;
                byte gy = (byte)(y & 0xFF);

                for (int x = 0; x < width; x++)
                {
                    byte* p = row + x * 4;
                    p[0] = (byte)(x & 0xFF);
                    p[1] = gy;
                    p[2] = (byte)((x + y) & 0xFF);
                    p[3] = 255;
                }
            }

            wb.AddDirtyRect(new Int32Rect(0, 0, width, height));
        }
        finally
        {
            wb.Unlock();
        }
    }

    // ---- UNSAFE + PARALLEL ----
    private static void FillGradientUnsafeParallel(WriteableBitmap wb)
    {
        wb.Lock();
        try
        {
            int width = wb.PixelWidth;
            int height = wb.PixelHeight;
            int stride = wb.BackBufferStride;
            IntPtr buffer = wb.BackBuffer;

            Parallel.For(0, height, y =>
            {
                unsafe
                {
                    byte* basePtr = (byte*)buffer;
                    byte* row = basePtr + y * stride;
                    byte gy = (byte)(y & 0xFF);

                    for (int x = 0; x < width; x++)
                    {
                        byte* p = row + x * 4;
                        p[0] = (byte)(x & 0xFF);
                        p[1] = gy;
                        p[2] = (byte)((x + y) & 0xFF);
                        p[3] = 255;
                    }
                }
            });

            wb.AddDirtyRect(new Int32Rect(0, 0, width, height));
        }
        finally
        {
            wb.Unlock();
        }
    }
}

実行結果

=== Result (Average) ===
Safe             : 298.10 ms
SafeParallel     : 144.10 ms
Unsafe           : 178.60 ms
UnsafeParallel   : 26.70 ms

感想

今回のベンチ結果を踏まえると 「10倍速い!」は誇張になるけれど、“選択肢として検討する価値が十分ある高速化”と言えるかと思います。
しかし、Unsafe+Parallelがキャッシュが効くことが確認できましたが、単発動作だと遅い可能性が考えられます。

コメント