【C#】同じ機能、違う書き方 - パフォーマンスで選ぶべきコードはどっち？

## はじめに

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

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

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

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

https://learn.microsoft.com/ja-jp/visualstudio/profiling/profiling-with-benchmark-dotnet?view=vs-2022

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

## 目次

- [ベンチマーク共通環境](#ベンチマーク共通環境)
- [1. 文字列操作の最適化](#1-文字列操作の最適化)
- [2. コレクション操作の最適化](#2-コレクション操作の最適化)
- [3. 非同期処理の最適化](#3-非同期処理の最適化)
- [4. 条件分岐の最適化](#4-条件分岐の最適化)
- [5. メモリアロケーションの最適化](#5-メモリアロケーションの最適化)
- [6. 数値計算の最適化](#6-数値計算の最適化)
- [7. キャッシュ効率の最適化](#7-キャッシュ効率の最適化)
- [まとめ](#まとめ)


## ベンチマーク共通環境
今回、測定した環境の情報は下記のとおりです。

| 項目 | 値 |
|------|----|
| **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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2253716/8c86558b-1888-4c39-b3a4-13dac082ce4d.png)

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

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

## 1. 文字列操作の最適化

### ❌ 非効率なアプローチ（+=演算子）
文字列の`+=`演算子を使用したクエリ文字列の構築

```csharp: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`を使用して効率的に文字列を構築

```csharp: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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2253716/407550e9-3588-4525-84ea-7565e67b46c9.png)


![BenchmarkDotNet summary](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2253716/bbc466e6-c41e-4ac2-9f34-4dd30baa5025.png)

<!-- open属性なし -->
<details><summary>実際の測定に利用したコード</summary>

```csharp: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>();
}
```

</details>


| Method      | Mean (µs) | Alloc (KB) | Ratio |
|-------------|----------:|-----------:|------:|
| Inefficient | 6.58      | 116.8      | 1.00 |
| Efficient   | **0.81**  | **6.1**    | **0.12** |

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

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

>＜参考記事＞
>[【C#】ZeroAllocationへの道 - 究極のメモリ最適化テクニック](https://qiita.com/Sakai_path/items/73633f0d0aaa58e69587)

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

### ❌ 非効率なアプローチ
LINQチェーンを使用したフィルタリングと変換（複数回の反復処理）

```csharp: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;
}
```

### ✅ 効率的なアプローチ
単一ループでフィルタリングと変換を同時実行

```csharp: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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2253716/fcc5daa4-3b02-4ad8-900a-d61fd4c33cc6.png)


![BenchmarkDotNet summary](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2253716/fe9f8ee2-8f93-4920-87ae-af27ce8e9e63.png)

<!-- open属性なし -->
<details><summary>実際の測定に利用したコード</summary>

```csharp: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;
    }
}

```

</details>

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

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

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

## 3. 非同期処理の最適化

### ❌ 非効率なアプローチ
`foreach`での順次`await`処理（直列実行）

```csharp: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`を使用した並列処理

```csharp: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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2253716/1a3826dd-b7fa-4607-b162-2db5e1e82e0e.png)


![BenchmarkDotNet summar](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2253716/373b196c-2381-4e18-b539-3e36d0b64356.png)

<!-- open属性なし -->
<details><summary>実際の測定に利用したコード</summary>

```csharp: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")
            };
        }
    }
}

```

</details>

| Method      | Mean (ms) | Ratio |
|-------------|----------:|------:|
| Inefficient | 1 093     | 1.00 |
| Efficient   | **109**   | **0.10** |

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

:::note
**【測定条件】**
- 実ネットワークの揺らぎを排除するため、`HttpMessageHandler` で **100 ms の `Task.Delay`** を入れた疑似 API を使用。  
- リクエスト本数：10 本 / ペイロード：文字列 `"dummy"` / .NET 8.0.11 / BenchmarkDotNet 0.15.1
:::


>＜参考記事＞
>[【C#】非同期プログラミングの正しい理解と実践](https://qiita.com/Sakai_path/items/a3065f4750cf3120b9c7)

## 4. 条件分岐の最適化

### ❌ 非効率なアプローチ
ネストした`if-else`文による条件分岐

```csharp: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式）の組み合わせ

```csharp: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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2253716/840c9fab-fb68-44d5-a420-b6ad4adb3c37.png)


![BenchmarkDotNet summary](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2253716/7f42d377-ed76-4250-846f-5ebcb0b193a3.png)

<!-- open属性なし -->
<details><summary>実際の測定に利用したコード</summary>

```csharp: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
        };
    }
}

```

</details>

| Iterations | Method | Mean&nbsp;(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** |

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

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


>＜参考記事＞
>[【C#】パターンマッチングで条件分岐を簡潔に書く](https://qiita.com/Sakai_path/items/c36e05bd7c51c94a02c7)

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

### ❌ 非効率なアプローチ
ピクセル毎にヒープに配列を作成する画像処理

```csharp: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コードによる直接メモリ操作

```csharp: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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2253716/8a65244a-42ec-445a-ad0f-2b2f3445fd1a.png)


![BenchmarkDotNet summary](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2253716/0c769fb7-bdd9-4d2e-9dae-9ce11e09bc4c.png)

<!-- open属性なし -->
<details><summary>実際の測定に利用したコード</summary>

```csharp: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]
    };
}

```

</details>

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

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

:::note
**【測定条件】**
- 画像サイズ：**3840 × 2160 × RGBA (≈ 32 MB)**  
- フィルタ：全チャネル反転（疑似処理）  
- .NET 8.0 Release／BenchmarkDotNet 0.15.1  
- `<AllowUnsafeBlocks>true</AllowUnsafeBlocks>` を有効化
:::

>＜参考記事＞
>[【C#】高速な画像処理を実現する方法 - unsafeと現代的アプローチの使い分け](https://qiita.com/Sakai_path/items/ea4b943acb494cfc9030)

## 6. 数値計算の最適化

### ❌ 非効率なアプローチ
`Math.Pow(x, 2)`を使用した距離計算

```csharp: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 * x`と`ReadOnlySpan<T>`の使用

```csharp: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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2253716/942d223f-42f3-42ed-b66a-6f7c23034f62.png)


![BenchmarkDotNet summary](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2253716/6bf15ade-0196-4521-a399-339557da9341.png)

<!-- open属性なし -->
<details><summary>実際の測定に利用したコード</summary>

```csharp: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; }
}

}

```

</details>

| PointCount | Method | Mean&nbsp;(µs) | Ratio |
|-----------:|:-------|---------------:|------:|
| **10&nbsp;000** | Inefficient | 264.1 | 1.00 |
|               | Efficient   | **13.5** | **0.05** |
| **100&nbsp;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** |

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

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

### ❌ 非効率なアプローチ
列優先（column-major）での行列アクセス

```csharp: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）での行列アクセス

```csharp: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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2253716/29a641b3-f833-4af4-8c75-2eee512b5ce9.png)

![BenchmarkDotNet summary](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2253716/b7b7cdef-9000-4976-bfa8-f58abe6ea6f4.png)

<!-- open属性なし -->
<details><summary>実際の測定に利用したコード</summary>

```csharp: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;
    }
}

```

</details>

| Method      | Mean (µs) | Ratio |
|-------------|----------:|------:|
| Inefficient | 832.9 | 1.00 |
| Efficient   | **555.6** | **0.67** |

:::note warn
**【結果】**
- 行優先アクセスは **約 1.5 倍高速**
:::

## まとめ
![a8.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2253716/67875232-1736-496a-b067-2b2134f57f54.png)

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

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

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

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

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

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

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

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

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

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