name: dotnet-benchmarkdotnet description: “运行 BenchmarkDotNet 微基准测试。设置、内存诊断器、基线、结果分析。” user-invocable: false
dotnet-benchmarkdotnet
使用 BenchmarkDotNet v0.14+ 进行 .NET 微基准测试的指南。涵盖基准类设置、内存和反汇编诊断器、用于 CI 工件收集的导出器、基线比较以及导致测量无效的常见陷阱。
版本假设: BenchmarkDotNet v0.14+ 基于 .NET 8.0+ 基线。示例使用当前稳定的 API。
范围
- 基准类设置和配置
- 内存和反汇编诊断器
- 用于 CI 工件收集的导出器
- 基线比较和结果分析
- 导致测量无效的常见陷阱
- 使用 [Params] 和基准类别的参数化基准测试
范围之外
- 性能架构模式(Span<T>、ArrayPool、sealed)——请参见 [skill:dotnet-performance-patterns]
- 性能分析工具(dotnet-counters、dotnet-trace、dotnet-dump)——请参见 [skill:dotnet-profiling]
- CI 基准测试回归检测——请参见 [skill:dotnet-ci-benchmarking]
- 原生 AOT 编译和性能——请参见 [skill:dotnet-native-aot]
- 序列化格式性能——请参见 [skill:dotnet-serialization]
- 架构模式(缓存、弹性)——请参见 [skill:dotnet-architecture-patterns]
- GC 调优和内存管理——请参见 [skill:dotnet-gc-memory]
交叉引用:用于基准测试测量的零分配模式,请参见 [skill:dotnet-performance-patterns];用于 Span/Memory 语法基础,请参见 [skill:dotnet-csharp-modern-patterns];用于 sealed 类样式约定,请参见 [skill:dotnet-csharp-coding-standards];用于 AOT 性能特性和基准测试考虑,请参见 [skill:dotnet-native-aot];用于序列化格式性能权衡,请参见 [skill:dotnet-serialization]。
包设置
<!-- Benchmarks.csproj -->
<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 列中的零确认零分配代码路径。
DisassemblyDiagnoser
检查 JIT 编译的汇编代码以验证优化(去虚拟化、内联):
[DisassemblyDiagnoser(maxDepth: 2)]
[MemoryDiagnoser]
public class DevirtualizationBenchmarks
{
// sealed 启用 JIT 去虚拟化——在反汇编输出中验证
// 有关 sealed 类约定,请参见 [skill:dotnet-csharp-coding-standards]
[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 验证 sealed 类是否从 JIT 获得去虚拟化,确认在 [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()
{
// 基准测试代码
}
}
导出器输出
| 导出器 | 文件 | 使用案例 |
|---|---|---|
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) // 为 CI 速度减少迭代次数
.AddExporter(JsonExporter.Full)
.WithArtifactsPath("./benchmark-results");
BenchmarkRunner.Run<CiBenchmarks>(config);
GitHub Actions 工件上传
- name: 运行基准测试
run: dotnet run -c Release --project benchmarks/MyBenchmarks.csproj
- name: 上传基准测试结果
uses: actions/upload-artifact@v4
with:
name: benchmark-results
path: benchmarks/BenchmarkDotNet.Artifacts/results/
retention-days: 30
基线比较
设置基线
标记一个基准测试作为基线以进行比率比较:
[MemoryDiagnoser]
public class SerializationBenchmarks
{
// 序列化格式选择——有关 API 详情,请参见 [skill:dotnet-serialization]
private readonly JsonSerializerOptions _options = new()
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
};
private readonly WeatherForecast _data = new()
{
Date = DateOnly.FromDateTime(DateTime.Now),
TemperatureC = 25,
Summary = "温暖"
};
[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 模式
运行特定基准测试
// 运行单个基准测试类
BenchmarkRunner.Run<StringConcatBenchmarks>();
// 运行程序集中的所有基准测试
BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args);
命令行过滤
# 运行匹配模式的基准测试
dotnet run -c Release -- --filter *StringBuilder*
# 列出所有可用基准测试而不运行
dotnet run -c Release -- --list flat
# 干运行(验证设置而不完整基准测试)
dotnet run -c Release -- --filter *StringBuilder* --job Dry
AOT 基准测试考虑
在基准测试原生 AOT 场景时,JIT 诊断器不可用(因为没有 JIT)。改为使用挂钟时间和内存比较。有关 AOT 编译设置,请参见 [skill:dotnet-native-aot]:
[MemoryDiagnoser]
// 不要与 AOT 一起使用 DisassemblyDiagnoser——没有 JIT 可反汇编
public class AotBenchmarks
{
[Benchmark]
public string SourceGenSerialize()
=> System.Text.Json.JsonSerializer.Serialize(
new { Value = 42 },
AppJsonContext.Default.Options);
}
常见陷阱
死代码消除
JIT 可能会消除未使用结果的基准测试代码。始终返回或使用结果:
// 不好:JIT 可能消除整个循环
[Benchmark]
public void DeadCode()
{
var sum = 0;
for (var i = 0; i < 1000; i++)
sum += i;
// sum 从未使用——JIT 移除循环
}
// 好:返回值以防止消除
[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](运行一次)vs [IterationSetup](每次迭代运行) |
| 环境噪音 | 后台进程扭曲结果 | BenchmarkDotNet 检测并警告环境问题;在嘈杂环境中使用 Job.MediumRun |
设置 vs 迭代生命周期
[MemoryDiagnoser]
public class LifecycleBenchmarks
{
private byte[] _data = null!;
[GlobalSetup] // 在所有基准测试迭代之前运行一次
public void GlobalSetup() => _data = new byte[1024];
[IterationSetup] // 在每个基准测试迭代之前运行
public void IterationSetup() => Array.Fill(_data, (byte)0);
[Benchmark]
public int Process()
{
// 使用 _data
return _data.Length;
}
[GlobalCleanup] // 在所有迭代之后运行一次
public void GlobalCleanup() { /* 释放资源 */ }
}
除非基准测试修改共享状态,否则优先使用 [GlobalSetup] 而非 [IterationSetup]。[IterationSetup] 添加的开销 BenchmarkDotNet 会从计时中排除,但仍影响 GC 压力测量。
代理注意事项
- 始终在 Release 模式下运行基准测试——
dotnet run -c Release。Debug 模式禁用 JIT 优化,产生无意义结果。 - 切勿在测试项目中运行基准测试——xUnit/NUnit 测试运行器干扰 BenchmarkDotNet 的测量框架。使用独立的控制台项目。
- 从基准测试方法返回值以防止死代码消除。JIT 将移除结果被丢弃的计算。
- 不要在基准测试中添加手动 Thread.Sleep 或 Task.Delay——BenchmarkDotNet 自动管理预热和迭代计时。
- 使用
[GlobalSetup]而非构造函数进行初始化——BenchmarkDotNet 在运行期间多次创建基准测试实例;构造函数代码会重复运行。 - 优先使用
[Params]而非手动循环进行参数化基准测试。BenchmarkDotNet 独立运行每个参数组合,并进行适当的统计分析。 - 导出 JSON 用于 CI——使用
[JsonExporterAttribute.Full]生成机器可读的工件以进行回归检测,而不仅仅是 Markdown。