dotnet-benchmarkdotnet について
.NET 8 以降と BenchmarkDotNet v0.14 以降を使う開発者向けのマイクロベンチマークスキルです。ベンチマーク設定、診断、比較、CI 向け出力と、測定の誤りを避ける方法を扱います。
主な機能
- 01ベンチマークの設定とパラメーター化
- 02メモリ割り当ての測定と JIT 出力の確認
- 03CI 成果物向けの結果出力
- 04ベースラインとの性能比較
- 05測定でよくある落とし穴の回避
活用シーン
- 01異なる実装の比較
- 02操作ごとのメモリ割り当ての測定
- 03逆アセンブルによる JIT 最適化の確認
- 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 | 操作ごとの割り当てバイト数 |
Gen0 | 1000 回の操作あたりの Gen 0 GC コレクション回数 |
Gen1 | 1000 回の操作あたりの Gen 1 GC コレクション回数 |
Gen2 | 1000 回の操作あたりの 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.Full | BenchmarkDotNet.Artifacts/results/*-report-full.json | CI での回帰比較(機械可読形式) |
HtmlExporter | BenchmarkDotNet.Artifacts/results/*-report.html | 人が読みやすい PR レビュー用成果物 |
MarkdownExporter | BenchmarkDotNet.Artifacts/results/*-report-github.md | PR コメントへの貼り付け |
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 圧力の測定には影響します。
エージェントが注意すべき点
- ベンチマークは必ず Release モードで実行してください --
dotnet run -c Releaseを使用します。Debug モードでは JIT 最適化が無効になり、意味のない結果になります。 - テストプロジェクトでベンチマークを実行しないでください -- xUnit/NUnit のテストランナーが BenchmarkDotNet の測定ハーネスに干渉します。独立したコンソールプロジェクトを使用してください。
- デッドコードの除去を防ぐため、ベンチマークメソッドから値を返してください。結果が破棄される計算は JIT によって除去されます。
- ベンチマークに手動で Thread.Sleep や Task.Delay を追加しないでください -- BenchmarkDotNet がウォームアップとイテレーションのタイミングを自動的に管理します。
- 初期化にはコンストラクターではなく
[GlobalSetup]を使用してください -- BenchmarkDotNet は実行中にベンチマークのインスタンスを複数回作成するため、コンストラクターのコードは繰り返し実行されます。 - パラメーター化したベンチマークには、手動ループより
[Params]を優先してください。BenchmarkDotNet は、適切な統計分析を行いながら、各パラメーターの組み合わせを個別に実行します。 - CI 用に JSON をエクスポートしてください -- 回帰検出に使える機械可読な成果物を生成するため、Markdown だけでなく
[JsonExporterAttribute.Full]を使用してください。