190
182

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

はじめに

プログラムを速くする――と聞くと、「アルゴリズムを大改造する」「マルチスレッド化する」といった大がかりな施策を思い浮かべがちです。

しかし実際には、日常的に書いている ちょっとしたコードの選択 が、そのままアプリ全体の体感速度を左右しているケースも少なくありません。

同じ結果を出す 2 つのコード、実は書き方ひとつで 10 倍以上 の差が開くことも!?

今回は、アプリでよく目にする “当たり前” の実装を例に取り、「どちらを選ぶとどれくらい速いのか?」 を BenchmarkDotNet で可視化しながら検証していきます。

ここでの「効率的なアプローチ」は、あくまでパフォーマンスの比較においてであって「非効率なアプローチ」が悪いというわけでは決してありまん。 最適化のゴールは 「闇雲に速くする」 ことではなく、「可読性・保守性とのバランスを取りながらベストプラクティスを選ぶ」ことだということは忘れないでおきましょう。

目次

ベンチマーク共通環境

今回、測定した環境の情報は下記のとおりです。

項目
CPU Intel(R) Core(TM) Ultra 7 155H 3.80 GHz
RAM 16.0 GB
OS Windows 11 Home 24H2
.NET SDK 8.0.403
BenchmarkDotNet v0.15.1

Summary

以降すべてのセクションで共通。
Release(x64) ビルド & Ctrl+F5 実行 で測定しています。

※測定に利用したコードもおいておきますので「◀実際の測定に利用したコード」をクリックして展開)、実装の判断材料としてぜひ活用してください。

1. 文字列操作の最適化

❌ 非効率なアプローチ(+=演算子)

文字列の+=演算子を使用したクエリ文字列の構築

C#
public string BuildQueryString_Inefficient(Dictionary<string, string> parameters)
{
    string result = "";
    foreach (var param in parameters)
    {
        result += $"{param.Key}={param.Value}&";
    }
    return result.TrimEnd('&');
}

✅ 効率的なアプローチ(StringBuilder)

StringBuilderを使用して効率的に文字列を構築

C#
public string BuildQueryString_Efficient(Dictionary<string, string> parameters)
{
    var sb = new StringBuilder(parameters.Count * 20); // 適切な初期容量
    foreach (var param in parameters)
    {
        sb.Append(param.Key).Append('=').Append(param.Value).Append('&');
    }
    if (sb.Length > 0)
        sb.Length--; // 最後の&を削除
    return sb.ToString();
}

【 パフォーマンス差(予想) 】
100個のパラメータで約10倍の性能差

【 理由 】
文字列の+=演算子は毎回新しい文字列オブジェクトを作成するため、O(n²)の時間計算量になります。StringBuilderを使用することで、内部バッファの再利用によりO(n)に改善されます。

:airplane: ベンチマーク結果(+=演算子 vs StringBuilder)

a1.png

BenchmarkDotNet summary

実際の測定に利用したコード
C#
using System;
using System.Collections.Generic;
using System.Text;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;

public class QueryStringBench
{
    private const int DictSize = 100;
    private readonly Dictionary<string,string> _params;

    public QueryStringBench()
        => _params = GenerateParams(DictSize, seed: 1234);

    // █ ベースライン(+= 連結)
    [Benchmark(Baseline = true)]
    public string Inefficient() => BuildQueryString_Inefficient(_params);

    // █ 改善版(StringBuilder)
    [Benchmark]
    public string Efficient()   => BuildQueryString_Efficient(_params);

    //──────────────── テスト対象コード ────────────────
    private static string BuildQueryString_Inefficient(Dictionary<string,string> p)
    {
        string result = string.Empty;
        foreach (var kv in p) result += $"{kv.Key}={kv.Value}&";
        return result.TrimEnd('&');
    }

    private static string BuildQueryString_Efficient(Dictionary<string,string> p)
    {
        var sb = new StringBuilder(p.Count * 20);
        foreach (var (k,v) in p) sb.Append(k).Append('=').Append(v).Append('&');
        if (sb.Length > 0) sb.Length--;
        return sb.ToString();
    }

    private static Dictionary<string,string> GenerateParams(int n, int seed)
    {
        var rnd = new Random(seed);
        var d = new Dictionary<string,string>(n);
        for (int i = 0; i < n; i++) d[$"key{i}"] = rnd.Next(0, 10_000).ToString();
        return d;
    }
}

public class Program
{
    public static void Main(string[] args)
        => BenchmarkRunner.Run<QueryStringBench>();
}
Method Mean (µs) Alloc (KB) Ratio
Inefficient 6.58 116.8 1.00
Efficient 0.81 6.1 0.12

【結果】
StringBuilder 版は 8 倍高速 / 19 倍省メモリ
Gen0 GC 回数も 9.5 → 0.5 に激減し、スループット向上だけでなく予測可能性も高まりました。

【補足】
定数+少数の変数を 1 式で +(または $"")→ string.Concat 1 回になるので十分速い
ループなどで何度も追記する場合のみ StringBuilder が効果的—この 2 択で使い分ければ OK です。

<参考記事>
【C#】ZeroAllocationへの道 - 究極のメモリ最適化テクニック

2. コレクション操作の最適化

❌ 非効率なアプローチ

LINQチェーンを使用したフィルタリングと変換(複数回の反復処理)

C#
public List<User> FilterAndTransform_Inefficient(List<User> users, int minAge)
{
    var filtered = users.Where(u => u.Age >= minAge).ToList();
    var transformed = filtered.Select(u => new User 
    { 
        Id = u.Id, 
        Name = u.Name.ToUpper(), 
        Age = u.Age 
    }).ToList();
    return transformed;
}

✅ 効率的なアプローチ

単一ループでフィルタリングと変換を同時実行

C#
public List<User> FilterAndTransform_Efficient(List<User> users, int minAge)
{
    var result = new List<User>(users.Count); // 適切な初期容量
    foreach (var user in users)
    {
        if (user.Age >= minAge)
        {
            result.Add(new User 
            { 
                Id = user.Id, 
                Name = user.Name.ToUpper(), 
                Age = user.Age 
            });
        }
    }
    return result;
}

【 パフォーマンス差(予想) 】
10,000件のデータで約3倍の性能差

【 理由 】
LINQ チェーンは中間結果を作成し、複数回の反復処理が発生します。単一ループにまとめることで、メモリ使用量と処理時間の両方を削減できます。

:airplane: ベンチマーク結果(10,000 件, Age ≥ 55)

a2.png

BenchmarkDotNet summary

実際の測定に利用したコード
C#
using System;
using System.Collections.Generic;
using System.Linq;
using BenchmarkDotNet.Attributes;

[MemoryDiagnoser]
public class CollectionBench
{
    private const int ItemCount = 10_000;
    private const int MinAge = 58;

    private List<User> _users = default!;

    [GlobalSetup]
    public void Setup()
    {
        var rnd = new Random(1234);
        _users = Enumerable.Range(0, ItemCount)
                           .Select(i => new User(i, $"user{i}", rnd.Next(10, 60)))
                           .ToList();
    }

    [Benchmark(Baseline = true)]
    public List<User> Inefficient()
        => FilterAndTransform_Inefficient(_users, MinAge);

    [Benchmark]
    public List<User> Efficient()
        => FilterAndTransform_Efficient(_users, MinAge);

    // --- Inefficient ---
    private static List<User> FilterAndTransform_Inefficient(
        List<User> users, int minAge)
    {
        var filtered = users.Where(u => u.Age >= minAge).ToList();
        return filtered.Select(u =>
            new User(u.Id, u.Name.ToUpperInvariant(), u.Age))
            .ToList();
    }

    // --- Efficient ---
    private static List<User> FilterAndTransform_Efficient(
        List<User> users, int minAge)
    {
        var result = new List<User>(users.Count / 16);   // ざっくり 6〜7 % を見積もって予約
        foreach (var u in users)
        {
            if (u.Age >= minAge)
                result.Add(new User(u.Id, u.Name.ToUpperInvariant(), u.Age));
        }
        return result;
    }
}

Method Mean (µs) Alloc (KB) Ratio
Inefficient 25.9 38.3 1.00
Efficient 16.2 31.8 0.63

【結果】

  • 単一ループ版は 1.6 倍高速/メモリ 17 % 削減
  • Gen0 GC 回数も 3.1 → 2.6 と減少
  • ヒット率が 10 % 程度以下 のケースでは、この差がアプリ全体のスループットに効いてくる。

【補足】

  • 抽出率が高い(例: Age ≥ 30)場合は差が 1 割前後まで縮小。
  • 可読性を優先して LINQ を採用する余地がある一方、
  • 低ヒット率 × 大量データ では単一ループのメリットが顕著になる。

3. 非同期処理の最適化

❌ 非効率なアプローチ

foreachでの順次await処理(直列実行)

C#
public async Task<List<string>> ProcessUrls_Inefficient(List<string> urls)
{
    var results = new List<string>();
    using var client = new HttpClient();
    
    foreach (var url in urls)
    {
        var response = await client.GetStringAsync(url); // 順次処理
        results.Add(ProcessResponse(response));
    }
    return results;
}

✅ 効率的なアプローチ

Task.WhenAllを使用した並列処理

C#
public async Task<List<string>> ProcessUrls_Efficient(List<string> urls)
{
    using var client = new HttpClient();
    
    var tasks = urls.Select(async url =>
    {
        var response = await client.GetStringAsync(url);
        return ProcessResponse(response);
    });
    
    return (await Task.WhenAll(tasks)).ToList(); // 並列処理
}

【 パフォーマンス差(予想) 】
10個のAPIコールで約8倍の性能差(ネットワーク遅延による)

【 理由 】
Task.WhenAllを使用することで、複数のHTTPリクエストを並列実行できます。I/Oバウンドな処理では、待機時間を有効活用することで劇的な性能向上が可能です。

:airplane: ベンチマーク結果(10 本 × 100 ms ダミー API)

a3.png

BenchmarkDotNet summar

実際の測定に利用したコード
C#
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using BenchmarkDotNet.Attributes;

[MemoryDiagnoser]
public class AsyncBench
{
    // ------------------------------------------------------------
    // ベンチマークの前提
    //
    // 非同期 I/O (HTTP) を正確に比較するには、外部ネットワークの
    // 揺らぎ(回線状況・サーバ処理時間など)を除く必要があります。
    // そこで今回は「実通信の代わりに 100 ms 待機するだけ」の
    // 疑似 HTTP ハンドラを用意し、純粋に
    //   ・逐次 await
    //   ・Task.WhenAll(…) による並列 await
    // がどれだけ差を生むかを検証します。
    // ------------------------------------------------------------

    private const int UrlCount = 10;         // 10 本の疑似 API
    private readonly List<string> _urls;

    public AsyncBench()
    {
        // ダミー URL を作っておくだけ(実際は使わない)
        _urls = Enumerable.Range(1, UrlCount)
                          .Select(i => $"https://example.com/api/{i}")
                          .ToList();
    }

    // ---------------- 疑似 HTTP クライアント ----------------
    private static readonly HttpClient FakeClient =
        new HttpClient(new FakeDelayHandler(TimeSpan.FromMilliseconds(100)))
        {
            BaseAddress = new Uri("https://example.com")
        };

    // ---------------- ベンチ対象 ----------------------------
    [Benchmark(Baseline = true)]
    public async Task<List<string>> Inefficient()
        => await ProcessUrls_Inefficient(_urls);

    [Benchmark]
    public async Task<List<string>> Efficient()
        => await ProcessUrls_Efficient(_urls);

    // ------ 非効率(逐次)-----------------------------------
    private static async Task<List<string>> ProcessUrls_Inefficient(
        List<string> urls)
    {
        var results = new List<string>();
        foreach (var url in urls)
        {
            var response = await FakeClient.GetStringAsync(url);
            results.Add(ProcessResponse(response));
        }
        return results;
    }

    // ------ 効率(並列)-------------------------------------
    private static async Task<List<string>> ProcessUrls_Efficient(
        List<string> urls)
    {
        var tasks = urls.Select(async url =>
        {
            var response = await FakeClient.GetStringAsync(url);
            return ProcessResponse(response);
        });

        return (await Task.WhenAll(tasks)).ToList();
    }

    // 疑似レスポンス加工
    private static string ProcessResponse(string s) => s.ToUpperInvariant();

    // ------ 100 ms 待機だけを行うダミー Handler --------------
    private sealed class FakeDelayHandler : HttpMessageHandler
    {
        private readonly TimeSpan _delay;
        public FakeDelayHandler(TimeSpan delay) => _delay = delay;

        protected override async Task<HttpResponseMessage> SendAsync(
            HttpRequestMessage request, CancellationToken cancellationToken)
        {
            await Task.Delay(_delay, cancellationToken);

            return new HttpResponseMessage()
            {
                Content = new StringContent("dummy")
            };
        }
    }
}

Method Mean (ms) Ratio
Inefficient 1 093 1.00
Efficient 109 0.10

【結果】

  • Task.WhenAll で 10 本を並列化すると 約 10 倍のスループット
  • CPU 使用率はほぼ変わらず、待機時間を“重ねて潰す”ことで劇的に短縮できる。

【測定条件】

  • 実ネットワークの揺らぎを排除するため、HttpMessageHandler100 ms の Task.Delay を入れた疑似 API を使用。
  • リクエスト本数:10 本 / ペイロード:文字列 "dummy" / .NET 8.0.11 / BenchmarkDotNet 0.15.1

<参考記事>
【C#】非同期プログラミングの正しい理解と実践

4. 条件分岐の最適化

❌ 非効率なアプローチ

ネストしたif-else文による条件分岐

C#
public decimal CalculateDiscount_Inefficient(CustomerType type, decimal amount)
{
    if (type == CustomerType.Regular)
    {
        if (amount > 10000) return amount * 0.05m;
        else if (amount > 5000) return amount * 0.03m;
        else return 0;
    }
    else if (type == CustomerType.Premium)
    {
        if (amount > 10000) return amount * 0.15m;
        else if (amount > 5000) return amount * 0.10m;
        else return amount * 0.05m;
    }
    else if (type == CustomerType.VIP)
    {
        if (amount > 10000) return amount * 0.25m;
        else if (amount > 5000) return amount * 0.20m;
        else return amount * 0.15m;
    }
    return 0;
}

✅ 効率的なアプローチ

辞書とパターンマッチング(switch式)の組み合わせ

C#
private static readonly Dictionary<CustomerType, (decimal high, decimal mid, decimal low)> 
    DiscountRates = new()
{
    [CustomerType.Regular] = (0.05m, 0.03m, 0.00m),
    [CustomerType.Premium] = (0.15m, 0.10m, 0.05m),
    [CustomerType.VIP] = (0.25m, 0.20m, 0.15m)
};

public decimal CalculateDiscount_Efficient(CustomerType type, decimal amount)
{
    if (!DiscountRates.TryGetValue(type, out var rates))
        return 0;

    return amount switch
    {
        > 10000 => amount * rates.high,
        > 5000 => amount * rates.mid,
        _ => amount * rates.low
    };
}

【 パフォーマンス差(予想)】
約2倍の性能差(複雑な条件ほど差が拡大)

【 理由 】
パターンマッチングと辞書を活用することで、条件分岐の回数を削減し、コードの可読性も向上します。静的な辞書により、ルックアップコストも最小化されます。

:airplane: ベンチマーク結果(ネスト if-else vs 辞書 + switch 式)

a4.png

BenchmarkDotNet summary

実際の測定に利用したコード
C#

using System;
using System.Collections.Generic;
using BenchmarkDotNet.Attributes;

public enum CustomerType { Regular, Premium, VIP }

[MemoryDiagnoser]
public class ConditionBench
{
    // 1 回のベンチで計算する件数(100k / 500k / 2M)
    [Params(100_000, 500_000, 2_000_000)]
    public int Iterations { get; set; }

    private readonly CustomerType[] _types = { CustomerType.Regular, CustomerType.Premium, CustomerType.VIP };
    private readonly Random _rnd = new(1234);

    // ───────────────────────────────
    // 非効率:多段 if-else
    // ───────────────────────────────
    [Benchmark(Baseline = true)]
    public decimal Inefficient()
    {
        decimal sum = 0;
        for (int i = 0; i < Iterations; i++)
        {
            var type = _types[_rnd.Next(_types.Length)];
            var amount = _rnd.Next(0, 20_000);
            sum += CalculateDiscount_Inefficient(type, amount);
        }
        return sum;
    }

    // ───────────────────────────────
    // 効率:辞書 + switch 式
    // ───────────────────────────────
    [Benchmark]
    public decimal Efficient()
    {
        decimal sum = 0;
        for (int i = 0; i < Iterations; i++)
        {
            var type = _types[_rnd.Next(_types.Length)];
            var amount = _rnd.Next(0, 20_000);
            sum += CalculateDiscount_Efficient(type, amount);
        }
        return sum;
    }

    // ----------- 対象メソッド -------------

    private static decimal CalculateDiscount_Inefficient(CustomerType type, decimal amount)
    {
        if (type == CustomerType.Regular)
        {
            if (amount > 10_000) return amount * 0.05m;
            else if (amount > 5_000) return amount * 0.03m;
            else return 0;
        }
        else if (type == CustomerType.Premium)
        {
            if (amount > 10_000) return amount * 0.15m;
            else if (amount > 5_000) return amount * 0.10m;
            else return amount * 0.05m;
        }
        else if (type == CustomerType.VIP)
        {
            if (amount > 10_000) return amount * 0.25m;
            else if (amount > 5_000) return amount * 0.20m;
            else return amount * 0.15m;
        }
        return 0;
    }

    private static readonly Dictionary<CustomerType, (decimal high, decimal mid, decimal low)> DiscountRates = new()
    {
        [CustomerType.Regular] = (0.05m, 0.03m, 0.00m),
        [CustomerType.Premium] = (0.15m, 0.10m, 0.05m),
        [CustomerType.VIP] = (0.25m, 0.20m, 0.15m)
    };

    private static decimal CalculateDiscount_Efficient(CustomerType type, decimal amount)
    {
        if (!DiscountRates.TryGetValue(type, out var rates))
            return 0;

        return amount switch
        {
            > 10_000 => amount * rates.high,
            > 5_000 => amount * rates.mid,
            _ => amount * rates.low
        };
    }
}

Iterations Method Mean (ms) Ratio
100 k Inefficient 3.88 1.00
Efficient 3.71 0.96
500 k Inefficient 18.83 1.00
Efficient 18.14 0.96
2 M Inefficient 74.10 1.00
Efficient 72.67 0.98

【結果】
辞書+switch 式はネスト if-else に対し、最大で約 4 % 高速、メモリ割当も 15 % 前後削減。

今回のベンチでは差は 約 4 % でしたが、これは「タイプ 3 種・金額 3 段」の小さな条件木だったためです。実システムでは 商品カテゴリ × 会員ランク × キャンペーンフラグ … と分岐が増えることが多く、テーブル駆動型(辞書/配列)のメリットが一気に大きくなります。上表のシナリオを参考に、“増えそうな分岐は最初からテーブル設計” を意識してみてください。

<参考記事>
【C#】パターンマッチングで条件分岐を簡潔に書く

5. メモリアロケーションの最適化

❌ 非効率なアプローチ

ピクセル毎にヒープに配列を作成する画像処理

C#
public byte[] ProcessImageData_Inefficient(byte[] imageData, int width, int height)
{
    var result = new byte[imageData.Length];
    
    for (int i = 0; i < imageData.Length; i += 4) // RGBA
    {
        var pixel = new byte[] { imageData[i], imageData[i+1], imageData[i+2], imageData[i+3] };
        var processed = ApplyFilter(pixel); // 毎回新しい配列を作成
        Array.Copy(processed, 0, result, i, 4);
    }
    
    return result;
}

✅ 効率的なアプローチ

stackallocとunsafeコードによる直接メモリ操作

C#
public unsafe byte[] ProcessImageData_Efficient(byte[] imageData, int width, int height)
{
    var result = new byte[imageData.Length];
    Span<byte> pixel = stackalloc byte[4]; // スタック上に確保
    
    fixed (byte* sourcePtr = imageData, resultPtr = result)
    {
        for (int i = 0; i < imageData.Length; i += 4)
        {
            // 直接メモリ操作でコピー
            *(uint*)(pixel.GetPinnableReference()) = *(uint*)(sourcePtr + i);
            
            ApplyFilterInPlace(pixel); // インプレース処理
            
            *(uint*)(resultPtr + i) = *(uint*)(pixel.GetPinnableReference());
        }
    }
    
    return result;
}

【 パフォーマンス差(予想)】
大きな画像(4K)で約5倍の性能差

【 理由 】
stackallocによりヒープアロケーションを回避し、unsafeコードで直接メモリ操作を行うことで、GCプレッシャーを大幅に削減できます。

:airplane: ベンチマーク結果(4K 画像 1 枚)

a5.png

BenchmarkDotNet summary

実際の測定に利用したコード
C#

using System;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using BenchmarkDotNet.Attributes;

[MemoryDiagnoser]
public class ImageBench
{
    private const int Width = 3840;
    private const int Height = 2160;
    private byte[] _image = default!;

    [GlobalSetup]
    public void Setup()
    {
        _image = new byte[Width * Height * 4];
        new Random(1234).NextBytes(_image);
    }

    // ───── 基準 (ヒープ 4B × ピクセル) ─────
    [Benchmark(Baseline = true)]
    public byte[] Inefficient() =>
        ProcessImageData_Inefficient(_image);

    // ───── 改善版 ─────
    [Benchmark]
    public byte[] Efficient() =>
        ProcessImageData_Efficient(_image);

    // --------------------------------------------------

    private static byte[] ProcessImageData_Inefficient(byte[] src)
    {
        var dst = new byte[src.Length];
        for (int i = 0; i < src.Length; i += 4)
        {
            var pixel = new byte[]
            {
                src[i], src[i + 1], src[i + 2], src[i + 3]
            };
            var p = ApplyFilter(pixel);               // 毎回ヒープ 4B
            Array.Copy(p, 0, dst, i, 4);
        }
        return dst;
    }

    // ============= ここを安全に書き直し =============
    private static unsafe byte[] ProcessImageData_Efficient(byte[] src)
    {
        var dst = GC.AllocateUninitializedArray<byte>(src.Length);
        fixed (byte* pSrc = src, pDst = dst)
        {
            for (int i = 0; i < src.Length; i += 4)
            {
                // アンアライン読取
                uint pixel = Unsafe.ReadUnaligned<uint>(pSrc + i);

                // 8bit * 3 チャネル反転 (ARGB little-endian)
                uint processed =
                    (pixel & 0xFF000000) |                    // A
                    ((0xFFu - ((pixel >> 16) & 0xFF)) << 16) |// R
                    ((0xFFu - ((pixel >> 8) & 0xFF)) << 8) |// G
                    (0xFFu - (pixel & 0xFF));        // B

                // アンアライン書込
                Unsafe.WriteUnaligned(pDst + i, processed);
            }
        }
        return dst;
    }

    // 元のフィルタ (参照のみ)
    private static byte[] ApplyFilter(byte[] p) => new byte[]
    {
        (byte)(255 - p[0]), (byte)(255 - p[1]), (byte)(255 - p[2]), p[3]
    };
}

Method Mean (ms) Alloc (MB) Ratio
Inefficient 86.89 537.9 1.00
Efficient 9.60 31.64 0.11

【結果】

  • stackalloc + 直接メモリ操作版は 約 9 倍高速
  • ヒープ割当は 538 MB → 32 MB(約 17 分の 1) に削減
  • GC も Gen0 42 500 回 → 938 回 と大幅に減り、GC ストップ・ザ・ワールド時間をほぼ解消できた

【測定条件】

  • 画像サイズ:3840 × 2160 × RGBA (≈ 32 MB)
  • フィルタ:全チャネル反転(疑似処理)
  • .NET 8.0 Release/BenchmarkDotNet 0.15.1
  • <AllowUnsafeBlocks>true</AllowUnsafeBlocks> を有効化

<参考記事>
【C#】高速な画像処理を実現する方法 - unsafeと現代的アプローチの使い分け

6. 数値計算の最適化

❌ 非効率なアプローチ

Math.Pow(x, 2)を使用した距離計算

C#
public double CalculateDistance_Inefficient(Point[] points)
{
    double totalDistance = 0;
    for (int i = 0; i < points.Length - 1; i++)
    {
        var dx = points[i+1].X - points[i].X;
        var dy = points[i+1].Y - points[i].Y;
        totalDistance += Math.Sqrt(Math.Pow(dx, 2) + Math.Pow(dy, 2)); // 重い計算
    }
    return totalDistance;
}

✅ 効率的なアプローチ

単純な乗算x * xReadOnlySpan<T>の使用

C#
public double CalculateDistance_Efficient(ReadOnlySpan<Point> points)
{
    double totalDistance = 0;
    for (int i = 0; i < points.Length - 1; i++)
    {
        var dx = points[i+1].X - points[i].X;
        var dy = points[i+1].Y - points[i].Y;
        totalDistance += Math.Sqrt(dx * dx + dy * dy); // 乗算の方が高速
    }
    return totalDistance;
}

【 パフォーマンス差(予想) 】
30% の性能差

【 理由 】
Math.Pow(x, 2)は汎用的な累乗関数のため、単純なx * xより重い処理になります。また、ReadOnlySpan<T>を使用することで、境界チェックの最適化も期待できます。

:airplane: ベンチマーク結果(総距離計算)

a6.png

BenchmarkDotNet summary

実際の測定に利用したコード
C#

using System;
using BenchmarkDotNet.Attributes;
using System.Runtime.InteropServices;

[MemoryDiagnoser]
public class DistanceBench
{
    // 配列サイズを 3 段階で計測
    [Params(10_000, 100_000, 1_000_000)]
    public int PointCount { get; set; }

    private Point[] _points = default!;

    [GlobalSetup]
    public void Setup()
    {
        var rnd = new Random(1234);

        _points = new Point[PointCount];
        for (int i = 0; i < PointCount; i++)
            _points[i] = new Point(rnd.NextDouble() * 1000,
                                   rnd.NextDouble() * 1000);
    }

    // ───── ベースライン:Math.Pow(x,2) ─────
    [Benchmark(Baseline = true)]
    public double Inefficient() => CalculateDistance_Inefficient(_points);

    // ───── 改善版:x*x & ReadOnlySpan ─────
    [Benchmark]
    public double Efficient() => CalculateDistance_Efficient(_points);

    // ────────────────────────────────
    private static double CalculateDistance_Inefficient(Point[] pts)
    {
        double sum = 0;
        for (int i = 0; i < pts.Length - 1; i++)
        {
            var dx = pts[i + 1].X - pts[i].X;
            var dy = pts[i + 1].Y - pts[i].Y;
            sum += Math.Sqrt(Math.Pow(dx, 2) + Math.Pow(dy, 2));
        }
        return sum;
    }

    private static double CalculateDistance_Efficient(Point[] pts)
    {
        ReadOnlySpan<Point> s = pts;
        double sum = 0;
        for (int i = 0; i < s.Length - 1; i++)
        {
            var dx = s[i + 1].X - s[i].X;
            var dy = s[i + 1].Y - s[i].Y;
            sum += Math.Sqrt(dx * dx + dy * dy);
        }
        return sum;
    }
}

// シンプルな Point 構造体
[StructLayout(LayoutKind.Sequential)]
public readonly struct Point
{
    public readonly double X;
    public readonly double Y;
    public Point(double x, double y) { X = x; Y = y; }
}

}

PointCount Method Mean (µs) Ratio
10 000 Inefficient 264.1 1.00
Efficient 13.5 0.05
100 000 Inefficient 2 625.8 1.00
Efficient 136.4 0.05
1 000 000 Inefficient 26 673.1 1.00
Efficient 1 553.2 0.06

【結果】

  • dx*dx + dy*dy 方式は 約 18〜20 倍高速
  • Math.Pow(dx, 2) は汎用累乗関数のため、専用の掛け算に比べ極端に遅い
  • ReadOnlySpan<Point> 採用により境界チェックも最適化され、追加のメモリ割当はゼロ(Alloc 列 0 B)

7. キャッシュ効率の最適化

❌ 非効率なアプローチ

列優先(column-major)での行列アクセス

C#
public int SumMatrix_Inefficient(int[,] matrix)
{
    int sum = 0;
    int rows = matrix.GetLength(0);
    int cols = matrix.GetLength(1);
    
    for (int col = 0; col < cols; col++) // 列優先アクセス
    {
        for (int row = 0; row < rows; row++)
        {
            sum += matrix[row, col];
        }
    }
    return sum;
}

✅ 効率的なアプローチ

行優先(row-major)での行列アクセス

C#
public int SumMatrix_Efficient(int[,] matrix)
{
    int sum = 0;
    int rows = matrix.GetLength(0);
    int cols = matrix.GetLength(1);
    
    for (int row = 0; row < rows; row++) // 行優先アクセス
    {
        for (int col = 0; col < cols; col++)
        {
            sum += matrix[row, col];
        }
    }
    return sum;
}

【 パフォーマンス差(予想)】
大きな行列(1000x1000)で約3倍の性能差

【 理由 】
C#の多次元配列は行優先(row-major)でメモリに配置されるため、行優先でアクセスすることでCPUキャッシュヒット率が向上します。

:airplane: ベンチマーク結果(1000 × 1000 int[,])

a7.png

BenchmarkDotNet summary

実際の測定に利用したコード
C#

using System;
using BenchmarkDotNet.Attributes;

[MemoryDiagnoser]
public class MatrixBench
{
    private const int Size = 1_000;          // 1000 × 1000 行列
    private int[,] _matrix = default!;

    [GlobalSetup]
    public void Setup()
    {
        _matrix = new int[Size, Size];
        var rnd = new Random(1234);
        for (int r = 0; r < Size; r++)
            for (int c = 0; c < Size; c++)
                _matrix[r, c] = rnd.Next(0, 100);
    }

    // ───── 列優先(キャッシュ効率 ×)─────
    [Benchmark(Baseline = true)]
    public int Inefficient() => SumMatrix_Inefficient(_matrix);

    // ───── 行優先(キャッシュ効率 ◎)─────
    [Benchmark]
    public int Efficient() => SumMatrix_Efficient(_matrix);

    // ----------- 対象メソッド ---------------

    private static int SumMatrix_Inefficient(int[,] m)
    {
        int sum = 0;
        int rows = m.GetLength(0);
        int cols = m.GetLength(1);

        for (int col = 0; col < cols; col++)        // 列優先アクセス
            for (int row = 0; row < rows; row++)
                sum += m[row, col];

        return sum;
    }

    private static int SumMatrix_Efficient(int[,] m)
    {
        int sum = 0;
        int rows = m.GetLength(0);
        int cols = m.GetLength(1);

        for (int row = 0; row < rows; row++)        // 行優先アクセス
            for (int col = 0; col < cols; col++)
                sum += m[row, col];

        return sum;
    }
}

Method Mean (µs) Ratio
Inefficient 832.9 1.00
Efficient 555.6 0.67

【結果】

  • 行優先アクセスは 約 1.5 倍高速

まとめ

a8.png

さて、この測定結果はいかがでしたでしょうか。

思ったとおり? 意外だった!? いろいろな思いがあると思いますが、私個人としては、なかなか実際に測定してみるなんて機会なかったので、「あれ? 思ったほど差がない・・」とか「あ、本当にこんなに早いのか」とか面白かったです。

パフォーマンス最適化は、アルゴリズムの理解、メモリ使用パターンの把握、そしてC#/.NETの内部動作に関する知識がとても大切です。

重要なポイントは以下の通り!

【 最適化の優先順位 】

  1. アルゴリズムの複雑さ - O(n²)からO(n)への改善が最も効果的
  2. メモリアロケーション - GCプレッシャーの削減
  3. キャッシュ効率 - メモリアクセスパターンの最適化
  4. 並列化 - I/Oバウンドな処理での並列実行

【 測定とプロファイリング 】
最適化を行う際は、必ずBenchmarkDotNetなどのツールで実際の性能を測定し、プロファイラでボトルネックを特定することが重要です。

C#
[Benchmark]
public void TestMethod_Baseline() => MethodA();

[Benchmark]
public void TestMethod_Optimized() => MethodB();

【 業務での適用指針 】

  • 可読性とのバランス - 過度な最適化は保守性を損なう
  • 適用箇所の選択 - ホットパスに集中する
  • 継続的な改善 - パフォーマンステストの自動化

これらのテクニックを適切に活用することで、アプリケーションの性能を大幅に改善できます!! ただし、最適化は常に測定に基づいて行い、可読性とのバランスを保つこようにしてください。

190
182
6

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
190
182

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?