水曜日, 6月 18, 2025
- Advertisment -
ホームニューステックニュース【C#】同じ機能、違う書き方 - パフォーマンスで選ぶべきコードはどっち? #プログラミング - Qiita

【C#】同じ機能、違う書き方 – パフォーマンスで選ぶべきコードはどっち? #プログラミング – Qiita



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

はじめに

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

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

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

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

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

目次

ベンチマーク共通環境

今回の測定に利用したコードもおいておきますので(details-折りたたみになっているので要展開)、実装の判断材料としてぜひ活用してください。

項目
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(Dictionarystring, string> parameters)
{
    string result = "";
    foreach (var param in parameters)
    {
        result += $"{param.Key}={param.Value}&";
    }
    return result.TrimEnd('&');
}

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

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

C#

public string BuildQueryString_Efficient(Dictionarystring, 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 Dictionarystring,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(Dictionarystring,string> p)
    {
        string result = string.Empty;
        foreach (var kv in p) result += $"{kv.Key}={kv.Value}&";
        return result.TrimEnd('&');
    }

    private static string BuildQueryString_Efficient(Dictionarystring,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 Dictionarystring,string> GenerateParams(int n, int seed)
    {
        var rnd = new Random(seed);
        var d = new Dictionarystring,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.RunQueryStringBench>();
}
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 ListUser> FilterAndTransform_Inefficient(ListUser> 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 ListUser> FilterAndTransform_Efficient(ListUser> users, int minAge)
{
    var result = new ListUser>(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 ListUser> _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 ListUser> Inefficient()
        => FilterAndTransform_Inefficient(_users, MinAge);

    [Benchmark]
    public ListUser> Efficient()
        => FilterAndTransform_Efficient(_users, MinAge);

    // --- Inefficient ---
    private static ListUser> FilterAndTransform_Inefficient(
        ListUser> 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 ListUser> FilterAndTransform_Efficient(
        ListUser> users, int minAge)
    {
        var result = new ListUser>(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 TaskListstring>> ProcessUrls_Inefficient(Liststring> urls)
{
    var results = new Liststring>();
    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 TaskListstring>> ProcessUrls_Efficient(Liststring> 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 Liststring> _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 TaskListstring>> Inefficient()
        => await ProcessUrls_Inefficient(_urls);

    [Benchmark]
    public async TaskListstring>> Efficient()
        => await ProcessUrls_Efficient(_urls);

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

    // ------ 効率(並列)-------------------------------------
    private static async TaskListstring>> ProcessUrls_Efficient(
        Liststring> 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 TaskHttpResponseMessage> 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 DictionaryCustomerType, (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 DictionaryCustomerType, (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];
    Spanbyte> 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.AllocateUninitializedArraybyte>(src.Length);
        fixed (byte* pSrc = src, pDst = dst)
        {
            for (int i = 0; i  src.Length; i += 4)
            {
                // アンアライン読取
                uint pixel = Unsafe.ReadUnaligneduint>(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
  • true を有効化

<参考記事>
【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の使用

C#

public double CalculateDistance_Efficient(ReadOnlySpanPoint> 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を使用することで、境界チェックの最適化も期待できます。

: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)
    {
        ReadOnlySpanPoint> 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 採用により境界チェックも最適化され、追加のメモリ割当はゼロ(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;
}

【 パフォーマンス差(予想)】
大きな行列(1000×1000)で約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();

【 業務での適用指針 】

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

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





Source link

Views: 0

RELATED ARTICLES

返事を書く

あなたのコメントを入力してください。
ここにあなたの名前を入力してください

- Advertisment -