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. 03导出结果用于 CI 制品
  4. 04与基线比较性能
  5. 05避免常见测量误区

适用场景

  1. 01比较不同实现方案
  2. 02测量每次操作的内存分配
  3. 03通过反汇编检查 JIT 优化
  4. 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.FullBenchmarkDotNet.Artifacts/results/*-report-full.jsonCI 回归比较(机器可读)
HtmlExporterBenchmarkDotNet.Artifacts/results/*-report.html便于人工阅读的 PR 审查工件
MarkdownExporterBenchmarkDotNet.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 注意事项

  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——使用 [JsonExporterAttribute.Full] 生成机器可读的产物,以便检测回归,而不只是生成 Markdown。

评论

支持 Markdown