.NETBenchmarkDotNet微基准测试技能 dotnet-benchmarkdotnet

这个技能提供了使用 BenchmarkDotNet 进行 .NET 微基准测试的完整指南,包括基准类设置、内存和反汇编诊断器、CI 导出器、基线比较和常见陷阱。适用于 .NET 8.0+ 和 BenchmarkDotNet v0.14+。关键词:.NET, BenchmarkDotNet, 微基准测试, 性能分析, 内存诊断, CI 集成, 基准比较, 软件开发测试

测试 0 次安装 0 次浏览 更新于 3/6/2026

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 压力测量。


代理注意事项

  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. 导出 JSON 用于 CI——使用 [JsonExporterAttribute.Full] 生成机器可读的工件以进行回归检测,而不仅仅是 Markdown。