关于 dotnet-benchmarkdotnet
面向使用 .NET 8+ 和 BenchmarkDotNet v0.14+ 的开发者的微基准测试技能,介绍基准设置、诊断、比较和 CI 导出,并说明如何避免无效测量。
主要功能
- 01设置基准并配置参数
- 02测量内存分配并检查 JIT 输出
- 03导出结果用于 CI 制品
- 04与基线比较性能
- 05避免常见测量误区
适用场景
- 01比较不同实现方案
- 02测量每次操作的内存分配
- 03通过反汇编检查 JIT 优化
- 04收集基准报告供 CI 审查
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ wshaddix/ dotnet-skills --skill dotnet-benchmarkdotnet安装后在 Claude Code 中输入/dotnet-benchmarkdotnet确认已加载。
详细介绍
dotnet-benchmarkdotnet
使用 BenchmarkDotNet v0.14+ 为 .NET 提供微基准测试指南。涵盖基准测试类设置、内存和反汇编诊断器、用于 CI 工件收集的导出器、基线比较,以及会导致测量结果无效的常见问题。
**版本假设:**以 .NET 8.0+ 为基线,使用 BenchmarkDotNet v0.14+。示例使用当前稳定版 API。
**不在范围内:**面向性能的架构模式(Span<T>、ArrayPool<T>、sealed 类去虚拟化)由本项目的配套技能负责——参见 [skill:dotnet-performance-patterns]。现代模式的 C# 语法(records、primary constructors)——参见 [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] 介绍基准测试测量的零分配模式;[skill:dotnet-csharp-modern-patterns] 介绍 Span/Memory 语法基础;[skill:dotnet-csharp-coding-standards] 介绍 sealed 类样式约定;[skill:dotnet-native-aot] 介绍 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
基线比较
设置基线
将一个基准测试标记为用于比率比较的基线:
[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](仅运行一次),而非 [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 */ }
}
除非基准测试会修改共享状态,否则优先使用 [GlobalSetup],而非 [IterationSetup]。BenchmarkDotNet 会将 [IterationSetup] 的开销排除在计时之外,但它仍会影响 GC 压力测量。
Agent 注意事项
- 始终在 Release 模式下运行基准测试——
dotnet run -c Release。Debug 模式会禁用 JIT 优化,导致结果毫无意义。 - 切勿在测试项目中进行基准测试——xUnit/NUnit 测试运行器会干扰 BenchmarkDotNet 的测量框架。请使用独立的控制台项目。
- 从基准测试方法返回值,以防止死代码消除。JIT 会移除结果被丢弃的计算。
- 不要在基准测试中添加手动 Thread.Sleep 或 Task.Delay——BenchmarkDotNet 会自动管理预热和迭代计时。
- 使用
[GlobalSetup]而非构造函数进行初始化——BenchmarkDotNet 会在一次运行期间多次创建基准测试实例;构造函数代码也会重复执行。 - 参数化基准测试优先使用
[Params],而非手动循环。BenchmarkDotNet 会独立运行每种参数组合,并进行适当的统计分析。 - 为 CI 导出 JSON——使用
[JsonExporterAttribute.Full]生成机器可读的产物,以便检测回归,而不只是生成 Markdown。