mcpField
D
dotnet-benchmarkdotnet

dotnet-benchmarkdotnet

github.com/wshaddixテスト・品質8680 回インストール35 週間前に更新
リポジトリ

dotnet-benchmarkdotnet について

.NET 8 以降と BenchmarkDotNet v0.14 以降を使う開発者向けのマイクロベンチマークスキルです。ベンチマーク設定、診断、比較、CI 向け出力と、測定の誤りを避ける方法を扱います。

主な機能

  1. 01ベンチマークの設定とパラメーター化
  2. 02メモリ割り当ての測定と JIT 出力の確認
  3. 03CI 成果物向けの結果出力
  4. 04ベースラインとの性能比較
  5. 05測定でよくある落とし穴の回避

活用シーン

  1. 01異なる実装の比較
  2. 02操作ごとのメモリ割り当ての測定
  3. 03逆アセンブルによる JIT 最適化の確認
  4. 04CI レビュー用ベンチマークレポートの収集

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/wshaddix/dotnet-skills --skill dotnet-benchmarkdotnet

インストール後、Claude Code で/dotnet-benchmarkdotnetを入力して読み込まれたか確認します。

詳細

dotnet-benchmarkdotnet

BenchmarkDotNet v0.14+ を使用した .NET のマイクロベンチマークのガイドです。ベンチマーククラスの設定、メモリ診断と逆アセンブル診断、CI での成果物収集用エクスポーター、ベースラインとの比較、測定値を無効にするよくある落とし穴を扱います。

バージョンの前提: BenchmarkDotNet v0.14+、.NET 8.0+ のベースラインです。例では現在の安定版 API を使用しています。

対象外: パフォーマンス重視のアーキテクチャパターン(Span<T>、ArrayPool<T>、sealed class の逆仮想化)は、このエピックの関連スキルで扱います。詳しくは [skill:dotnet-performance-patterns] を参照してください。最新のパターン(records、primary constructors)の C# 構文については、[skill:dotnet-csharp-modern-patterns] を参照してください。コーディング標準とスタイル規約については、[skill:dotnet-csharp-coding-standards] を参照してください。Native AOT のコンパイルパイプラインとパフォーマンス特性については、[skill:dotnet-native-aot] を参照してください。シリアライズ形式の API とラウンドトリップの正確性については、[skill:dotnet-serialization] を参照してください。プロファイリングツール(dotnet-counters、dotnet-trace、dotnet-dump)については、[skill:dotnet-profiling] で扱います。CI でのベンチマーク回帰検出については、[skill:dotnet-ci-benchmarking] で扱います。アーキテクチャパターン(キャッシュ、レジリエンス)については、[skill:dotnet-architecture-patterns] を参照してください。EF Core クエリの最適化については、[skill:dotnet-efcore-patterns] を参照してください。

相互参照:ベンチマークで測定するゼロアロケーションパターンについては [skill:dotnet-performance-patterns]、Span/Memory 構文の基礎については [skill:dotnet-csharp-modern-patterns]、sealed class のスタイル規約については [skill:dotnet-csharp-coding-standards]、AOT のパフォーマンス特性とベンチマークに関する考慮事項については [skill:dotnet-native-aot]、シリアライズ形式のパフォーマンス上のトレードオフについては [skill:dotnet-serialization] を参照してください。


パッケージのセットアップ


<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="BenchmarkDotNet" Version="0.14.*" />
  </ItemGroup>
</Project>

ベンチマークプロジェクトは本番コードと分離してください。ソリューションのルートに benchmarks/ ディレクトリを使用します。


ベンチマーククラスのセットアップ

[Benchmark] 属性を使った基本的なベンチマーク
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;

[MemoryDiagnoser]
public class StringConcatBenchmarks
{
    private readonly string[] _items = Enumerable.Range(0, 100)
        .Select(i => i.ToString())
        .ToArray();

    [Benchmark(Baseline = true)]
    public string StringConcat()
    {
        var result = string.Empty;
        foreach (var item in _items)
            result += item;
        return result;
    }

    [Benchmark]
    public string StringBuilder()
    {
        var sb = new System.Text.StringBuilder();
        foreach (var item in _items)
            sb.Append(item);
        return sb.ToString();
    }

    [Benchmark]
    public string StringJoin() => string.Join(string.Empty, _items);
}
ベンチマークの実行
// Program.cs
using BenchmarkDotNet.Running;

BenchmarkRunner.Run<StringConcatBenchmarks>();

有効な結果を得るには、Release モードで実行してください(必須):

dotnet run -c Release
パラメーター化されたベンチマーク
[MemoryDiagnoser]
public class CollectionBenchmarks
{
    [Params(10, 100, 1000)]
    public int Size { get; set; }

    private int[] _data = null!;

    [GlobalSetup]
    public void Setup()
    {
        _data = Enumerable.Range(0, Size).ToArray();
    }

    [Benchmark(Baseline = true)]
    public int ForLoop()
    {
        var sum = 0;
        for (var i = 0; i < _data.Length; i++)
            sum += _data[i];
        return sum;
    }

    [Benchmark]
    public int LinqSum() => _data.Sum();
}

メモリ診断

MemoryDiagnoser

ベンチマークの呼び出しごとに、GC の割り当て量とコレクション回数を追跡します。すべてのベンチマークに適用するには、クラスレベルで指定します:

[MemoryDiagnoser]
public class AllocationBenchmarks
{
    [Benchmark]
    public byte[] AllocateArray() => new byte[1024];

    [Benchmark]
    public int UseStackalloc()
    {
        Span<byte> buffer = stackalloc byte[1024];
        buffer[0] = 42;
        return buffer[0];
    }
}

出力列:

列意味
Allocated操作ごとの割り当てバイト数
Gen01000 回の操作あたりの Gen 0 GC コレクション回数
Gen11000 回の操作あたりの Gen 1 GC コレクション回数
Gen21000 回の操作あたりの Gen 2 GC コレクション回数

Allocated 列が 0 であれば、ゼロアロケーションのコードパスであることを確認できます。

DisassemblyDiagnoser

JIT コンパイルされたアセンブリを調べ、最適化(逆仮想化、インライン化)を確認します:

[DisassemblyDiagnoser(maxDepth: 2)]
[MemoryDiagnoser]
public class DevirtualizationBenchmarks
{
    // sealed enables JIT devirtualization -- verify in disassembly output
    // See [skill:dotnet-csharp-coding-standards] for sealed class conventions
    [Benchmark]
    public int SealedCall()
    {
        var obj = new SealedService();
        return obj.Calculate(42);
    }

    [Benchmark]
    public int VirtualCall()
    {
        IService obj = new SealedService();
        return obj.Calculate(42);
    }
}

public interface IService { int Calculate(int x); }
public sealed class SealedService : IService
{
    public int Calculate(int x) => x * 2;
}

DisassemblyDiagnoser を使用して、JIT によって sealed クラスが逆仮想化されることを確認できます。これにより、[skill:dotnet-csharp-coding-standards] に記載されているパフォーマンス上の根拠を確かめられます。


CI 統合用エクスポーター

エクスポーターの設定
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Exporters;
using BenchmarkDotNet.Exporters.Json;

[MemoryDiagnoser]
[JsonExporterAttribute.Full]
[HtmlExporter]
[MarkdownExporter]
public class CiBenchmarks
{
    [Benchmark]
    public void MyOperation()
    {
        // benchmark code
    }
}
エクスポーターの出力
エクスポーターファイル用途
JsonExporterAttribute.FullBenchmarkDotNet.Artifacts/results/*-report-full.jsonCI での回帰比較(機械可読形式)
HtmlExporterBenchmarkDotNet.Artifacts/results/*-report.html人が読みやすい PR レビュー用成果物
MarkdownExporterBenchmarkDotNet.Artifacts/results/*-report-github.mdPR コメントへの貼り付け
CI 用のカスタム設定
using BenchmarkDotNet.Configs;
using BenchmarkDotNet.Exporters.Json;
using BenchmarkDotNet.Jobs;

var config = ManualConfig.Create(DefaultConfig.Instance)
    .AddJob(Job.ShortRun)  // fewer iterations for CI speed
    .AddExporter(JsonExporter.Full)
    .WithArtifactsPath("./benchmark-results");

BenchmarkRunner.Run<CiBenchmarks>(config);
GitHub Actions での成果物のアップロード
- name: Run benchmarks
  run: dotnet run -c Release --project benchmarks/MyBenchmarks.csproj

- name: Upload benchmark results
  uses: actions/upload-artifact@v4
  with:
    name: benchmark-results
    path: benchmarks/BenchmarkDotNet.Artifacts/results/
    retention-days: 30

ベースラインとの比較

ベースラインの設定

比率を比較するため、1 つのベンチマークをベースラインとして指定します:

[MemoryDiagnoser]
public class SerializationBenchmarks
{
    // Serialization format choice -- see [skill:dotnet-serialization] for API details
    private readonly JsonSerializerOptions _options = new()
    {
        PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
    };

    private readonly WeatherForecast _data = new()
    {
        Date = DateOnly.FromDateTime(DateTime.Now),
        TemperatureC = 25,
        Summary = "Warm"
    };

    [Benchmark(Baseline = true)]
    public string SystemTextJson()
        => System.Text.Json.JsonSerializer.Serialize(_data, _options);

    [Benchmark]
    public byte[] Utf8Serialization()
        => System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(_data, _options);
}

public record WeatherForecast
{
    public DateOnly Date { get; init; }
    public int TemperatureC { get; init; }
    public string? Summary { get; init; }
}

出力の Ratio 列には、ベースライン(1.00)に対するパフォーマンスが表示されます。1.00 未満はベースラインより高速、1.00 を超える値はベースラインより低速であることを示します。

ベンチマークのカテゴリ

[BenchmarkCategory] でベンチマークをグループ化し、実行時にフィルターします:

[MemoryDiagnoser]
[GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory)]
public class CategorizedBenchmarks
{
    [Benchmark, BenchmarkCategory("Serialization")]
    public string JsonSerialize() => "...";

    [Benchmark, BenchmarkCategory("Allocation")]
    public byte[] ArrayAlloc() => new byte[1024];
}

特定のカテゴリを実行します:

dotnet run -c Release -- --filter *Serialization*

BenchmarkRunner.Run のパターン

特定のベンチマークの実行
// Run a single benchmark class
BenchmarkRunner.Run<StringConcatBenchmarks>();

// Run all benchmarks in assembly
BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args);
コマンドラインフィルタリング
# Run benchmarks matching a pattern
dotnet run -c Release -- --filter *StringBuilder*

# List all available benchmarks without running
dotnet run -c Release -- --list flat

# Dry run (validates setup without full benchmark)
dotnet run -c Release -- --filter *StringBuilder* --job Dry
AOT ベンチマークに関する考慮事項

Native AOT のシナリオをベンチマークする場合、JIT 診断機能は利用できません(JIT が存在しないためです)。代わりに、実時間とメモリ使用量を比較してください。AOT コンパイルの設定については、[skill:dotnet-native-aot] を参照してください。

[MemoryDiagnoser]
// Do NOT use DisassemblyDiagnoser with AOT -- no JIT to disassemble
public class AotBenchmarks
{
    [Benchmark]
    public string SourceGenSerialize()
        => System.Text.Json.JsonSerializer.Serialize(
            new { Value = 42 },
            AppJsonContext.Default.Options);
}

よくある落とし穴

デッドコードの除去

JIT は、結果が使われていないベンチマークコードを除去することがあります。必ず結果を返すか、使用してください。

// BAD: JIT may eliminate the entire loop
[Benchmark]
public void DeadCode()
{
    var sum = 0;
    for (var i = 0; i < 1000; i++)
        sum += i;
    // sum is never used -- JIT removes the loop
}

// GOOD: return the value to prevent elimination
[Benchmark]
public int LiveCode()
{
    var sum = 0;
    for (var i = 0; i < 1000; i++)
        sum += i;
    return sum;
}
測定の偏り
落とし穴原因対処法
Debug モードで実行するJIT 最適化が適用されない常に -c Release を使用する
変更可能な状態を共有するベンチマーク同士が干渉する[IterationSetup] を使うか、イミュータブルなデータを使用する
コールドスタートを測定する初回の実行には JIT コンパイルが含まれるBenchmarkDotNet がウォームアップを自動的に処理します。手動でウォームアップを追加しないでください
セットアップ時に割り当てるセットアップ時の割り当てによって Allocated 列の値が増える[GlobalSetup](1 回実行)を使う。[IterationSetup](イテレーションごとに実行)とは異なります
環境ノイズがあるバックグラウンドプロセスによって結果が偏るBenchmarkDotNet は環境の問題を検出して警告します。ノイズの多い環境では Job.MediumRun を使用してください
セットアップとイテレーションのライフサイクル
[MemoryDiagnoser]
public class LifecycleBenchmarks
{
    private byte[] _data = null!;

    [GlobalSetup]    // Runs once before all benchmark iterations
    public void GlobalSetup() => _data = new byte[1024];

    [IterationSetup] // Runs before each benchmark iteration
    public void IterationSetup() => Array.Fill(_data, (byte)0);

    [Benchmark]
    public int Process()
    {
        // uses _data
        return _data.Length;
    }

    [GlobalCleanup]    // Runs once after all iterations
    public void GlobalCleanup() { /* dispose resources */ }
}

ベンチマークで共有状態を変更する場合を除き、[IterationSetup] より [GlobalSetup] を優先してください。[IterationSetup] によるオーバーヘッドは BenchmarkDotNet の計測時間から除外されますが、GC 圧力の測定には影響します。


エージェントが注意すべき点

  1. ベンチマークは必ず Release モードで実行してください -- dotnet run -c Release を使用します。Debug モードでは JIT 最適化が無効になり、意味のない結果になります。
  2. テストプロジェクトでベンチマークを実行しないでください -- xUnit/NUnit のテストランナーが BenchmarkDotNet の測定ハーネスに干渉します。独立したコンソールプロジェクトを使用してください。
  3. デッドコードの除去を防ぐため、ベンチマークメソッドから値を返してください。結果が破棄される計算は JIT によって除去されます。
  4. ベンチマークに手動で Thread.Sleep や Task.Delay を追加しないでください -- BenchmarkDotNet がウォームアップとイテレーションのタイミングを自動的に管理します。
  5. 初期化にはコンストラクターではなく [GlobalSetup] を使用してください -- BenchmarkDotNet は実行中にベンチマークのインスタンスを複数回作成するため、コンストラクターのコードは繰り返し実行されます。
  6. パラメーター化したベンチマークには、手動ループより [Params] を優先してください。BenchmarkDotNet は、適切な統計分析を行いながら、各パラメーターの組み合わせを個別に実行します。
  7. CI 用に JSON をエクスポートしてください -- 回帰検出に使える機械可読な成果物を生成するため、Markdown だけでなく [JsonExporterAttribute.Full] を使用してください。

コメント