mcpField
M
microbenchmarking

microbenchmarking

github.com/dotnet测试与质量5.8k3.6k 次安装1 天前更新
代码仓库

关于 microbenchmarking

microbenchmarking 是指导使用 BenchmarkDotNet 编写和运行微基准测试的技能,适用于需要比较实现、运行时或配置性能的 .NET 开发者。它强调按测量目标设计用例,并先查阅相关参考资料以遵循当前行为。

主要功能

  1. 01规划基准测试的比较维度
  2. 02选择参数化和输入数据方式
  3. 03配置 BenchmarkDotNet 入口与选项
  4. 04控制测试用例数量和运行耗时
  5. 05校验结果并阅读报告

适用场景

  1. 01比较不同实现的性能
  2. 02调查特定性能问题
  3. 03评估代码变更的性能影响
  4. 04为开发反馈创建临时基准测试

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/dotnet/skills --skill microbenchmarking

安装后在 Claude Code 中输入/microbenchmarking确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

references/
bdn-internals-and-tuning.md
comparison-strategies.md
diagnosers-and-exporters.md
project-setup-and-running.md
writing-benchmarks.md

详细介绍

基准测试编写指南

BenchmarkDotNet(BDN)是一个用于编写和运行微基准测试的 .NET 库。在本技能中,“BDN”均指 BenchmarkDotNet。

注意: 对使用 BenchmarkDotNet 编写基准测试的 LLM 进行评估后,发现了一些常见的失败模式。这些问题源于对 BDN 行为的过时假设,尤其涉及运行时比较、作业配置以及在较新版本中已有变化的执行默认设置。本技能中的参考文件包含经过验证的最新信息。编写任何代码之前,必须先阅读与任务相关的参考文件——你的训练数据很可能包含过时或错误的 BDN 用法。

核心概念

  • 作业(Job)——描述如何运行基准测试:运行时、迭代次数、启动次数、运行策略和环境设置。可以配置多个作业,在不同条件下运行相同的基准测试。
  • 基准测试用例(Benchmark case)——一个方法 × 一种参数组合 × 一个作业。BDN 测量的最小单位。
  • 操作(Operation)——正在测量的逻辑工作单元。所有 BDN 输出列(Mean、Error 等)都报告每次操作的时间。
  • 调用(Invocation)——对基准测试方法的一次调用。默认情况下,1 次调用 = 1 次操作。使用 OperationsPerInvoke=N 时,每次调用计为 N 次操作。
  • 迭代(Iteration)——一批计时调用。BDN 会测量一次迭代中所有调用的总时间,再除以操作总数,得到每次操作的时间。

基准测试是比较工具

单个基准测试数值的价值有限——它可以确认测量值的数量级,但具体数值会因机器、操作系统和运行时配置而变化。将基准测试与某个对象进行比较,才能获得最有用的信息。编写基准测试之前,请确定当前任务的比较维度:

  • 方法(A 与 B):在同一次运行中并排比较不同的实现。
  • 运行时:比较相同代码在不同 .NET 版本上的表现(例如,net8.0 与 net9.0)。
  • 包版本:比较 NuGet 依赖项的不同版本。
  • 构建版本(前后对比):将旧代码保存的 DLL 与当前源代码进行比较。
  • 运行时配置(GC 模式、JIT 设置):了解运行时设置对性能的影响——通过在一次运行中使用多个作业进行比较。
  • 规模(N=100 与 N=1000):了解输入大小增大时性能如何变化。
  • 硬件/操作系统:在不同机器或操作系统之间进行比较——需要在每种环境中分别运行。
  • 历史测量结果:与之前某个时间点记录的测量结果进行比较。

BDN 可以在一次运行中并排比较前六种维度,但每种比较都需要特定的 CLI 标志或配置,这些可能与你的预期不同。配置比较之前,请阅读 references/comparison-strategies.md,了解每种策略的正确做法。

用例和基准测试生命周期

开发者编写基准测试有四种不同的原因,每种原因都会影响基准测试的设计方式和存放位置:

  1. 覆盖率套件:编写基准测试,尽可能覆盖真实世界的使用模式,以便捕获影响大多数用户的回归问题。这些基准测试是永久性的——它们属于项目的基准测试套件,遵循项目的惯例(目录结构、基类、命名),并提交到代码库中。

  2. 问题调查:有人报告了特定的性能问题。编写基准测试以复现并诊断该问题。这些基准测试的作用范围仅限于当前任务——它们会在调查期间持续存在(复现 → 隔离 → 验证修复),但不会成为永久套件的一部分。

  3. 变更验证:开发者有一个 PR 或变更,希望在合并前了解其性能特征。这些基准测试的作用范围仅限于当前任务——它们会在评审周期内持续存在,但不会提交到代码库中。

  4. 开发反馈:开发者正在处理一项任务,希望使用基准测试评估不同方案并尽早获取信息。这些基准测试的作用范围仅限于当前任务,且会在任务结束后丢弃——它们会在开发过程中持续存在,但会在做出决定后删除。

对于用例 1,请遵循现有基准测试项目的惯例,将基准测试添加到项目中。对于用例 2–4,请在工作目录中创建独立项目,在任务期间保留,但要明确表明它不属于永久代码库。

对于覆盖率套件基准测试,请从实际调用方的角度进行设计——哪些代码模式会使用此 API,他们会传入哪些输入,以及哪些性能特征对他们至关重要。每个永久基准测试都应通过其与真实世界的相关性,证明其维护成本是合理的。对于临时基准测试,应有意控制测试用例数量——每增加一个测试用例,都会增加实际耗时(请参阅成本意识)。

成本意识

使用默认设置时,每个基准测试用例(一个方法 × 一种参数组合 × 一个作业)需要 15–25 秒。[Params] 会生成笛卡尔积:两个 [Params] 分别有 3 个和 4 个值,应用于 5 个方法时,共有 60 个用例,耗时约 20 分钟。多个作业会进一步增加总耗时。运行前,请估算用例总数,并根据情况选择作业预设:

预设每个用例耗时适用场景
--job Dry<1s验证正确性——确认可以编译和执行,但不进行测量
--job Short5–8s在开发或调查期间快速测量
(默认)15–25s为覆盖率套件进行最终测量
--job Medium33–52s在结果至关重要时提高可信度
--job Long3–12 min获得较高的统计可信度

如果基准测试运行时间超出预期、结果似乎不稳定,或者你需要调整迭代次数或执行设置,请阅读 references/bdn-internals-and-tuning.md,了解 BDN 的执行流程和配置选项的详细信息。

入口点和配置

BDN 程序使用 BenchmarkSwitcher(为用户提供交互式基准测试选择功能,并解析 CLI 参数)或 BenchmarkRunner(直接运行指定的基准测试)。两者都支持 --filter 和 --runtimes 等 CLI 标志,但只有传入 args 时才会生效——不传入时,CLI 标志会被静默忽略。使用 BenchmarkSwitcher 时,务必传入 --filter,以避免卡在交互式提示中。

BDN 的行为可通过特性、配置对象和 CLI 标志进行自定义。

有关入口点设置、配置对象模式和 CLI 标志,请参阅 references/project-setup-and-running.md。如果你需要收集运行时间以外的数据,例如内存分配、硬件计数器或性能分析跟踪,请参阅 references/diagnosers-and-exporters.md。

运行基准测试

BenchmarkDotNet 的控制台输出极其冗长——每个用例都会输出数百行内部校准、预热和测量细节。将所有输出重定向到文件,以避免冗长的迭代输出占用上下文:

dotnet run -c Release -- --filter "*MethodName" --noOverwrite > benchmark.log 2>&1

每个基准测试方法可能需要数分钟才能完成。不要一次运行所有基准测试,而应使用 --filter 一次运行一部分(例如,每次调用运行一个或两个方法),读取结果后再运行下一部分。这样可以缩短每次调用的时间,避免会话或终端超时,并让你逐步验证结果。有关筛选器语法、CLI 标志和项目设置,请参阅 references/project-setup-and-running.md。

每次运行后,从结果目录中读取 Markdown 报告(*-report-github.md)以查看汇总表。只有在需要调查错误或意外结果时,才读取 benchmark.log。

编写新的基准测试

第 1 步:规划测试用例

编写任何代码之前,先确定:

  • 此基准测试的用途(覆盖率、调查、变更验证或开发反馈)。
  • 适用的比较维度(这个数值要与什么进行比较?)。
  • 根据调用方实际使用 API 的方式,确定要测试的真实场景。

每个基准测试用例都应证明其成本合理。通常,覆盖一个尚未测试的场景,比为一个已覆盖的场景再添加一组参数组合更有价值;但如果某个特定参数维度确实会影响性能特征,那么进行深入测试就是合理的。

确定测试用例列表。针对每个测试用例,请考虑:

  • 如何表达变化:BenchmarkDotNet 提供了多种参数化基准测试的机制——属性级参数 [Params] 和 [ParamsSource]、方法级参数 [Arguments] 和 [ArgumentsSource]、用于枚举 bool 或枚举类型所有值的 [ParamsAllValues],以及用于泛型基准测试类中改变类型参数的 [GenericTypeArguments]。选择最适合所测维度的机制。完整选项和正确性模式请参阅 references/writing-benchmarks.md。
  • 输入数据的来源——考虑适合的数据来源(也可以组合使用):
    • 硬编码值——适用于较小且固定、精确输入很重要的值(例如特定字符串、已知的边界情况大小)。将它们存储在字段或 [Params] 中,以避免常量折叠。
    • 资源文件——适用于过大或不便嵌入源代码的静态数据,例如二进制数据块。
    • 通过 [ParamsSource]/[ArgumentsSource]/[GlobalSetup] 以编程方式生成——适用于数据形态比具体内容更重要,或输入必须按大小参数化的情况。
  • 随机性是否合适:如果使用生成的数据,请采用带种子的随机数生成方式,以确保结果可复现。生成随机数据时,样本量应足够大,以确保生成的分布具有代表性(例如,4 个随机值可能集中在较窄的范围内,而 1000 个随机值能更全面地覆盖整个分布)。
第 2 步:实现基准测试

对于覆盖率套件基准测试,请添加到现有的基准测试项目中,并遵循其约定。对于临时基准测试(调查、变更验证、开发反馈),请创建独立项目——项目设置和入口点配置请参阅 references/project-setup-and-running.md。

添加 BenchmarkDotNet 包:始终使用 dotnet add package BenchmarkDotNet(不指定版本),以便 NuGet 解析最新的兼容版本。不要在 .csproj 中手动编写带版本号的 <PackageReference>;训练数据中的 BDN 版本已经过时,可能不支持当前的 .NET 运行时。

编写基准测试代码。遵循 references/writing-benchmarks.md 中的模式,避免常见的测量错误,尤其要注意:

  • 返回结果:从基准测试方法返回结果,以防止死代码消除。
  • 将初始化移至 [GlobalSetup]——在基准测试方法内部进行设置会被计入测量;只有在基准测试会修改状态且必须在每次迭代之间重置时,才使用 [IterationSetup]。
  • 不要添加手动循环——BDN 会自动控制调用次数。
  • 比较不同方案时标记基线——方法级比较使用 [Benchmark(Baseline = true)],多作业比较使用作业上的 .AsBaseline(),以便结果显示相对比率。
  • 将输入存储在字段或 [Params] 中,不要使用字面量或 const 值——JIT 可能会在编译时折叠常量表达式,导致基准测试测量的是预先计算的结果,而非实际计算过程。
步骤 3:验证并运行

在提交长时间运行前进行验证:

  1. 先使用 --job Dry 运行,以便发现编译错误和运行时异常,避免在测量上浪费时间。
  2. 使用默认设置运行一个有代表性的案例,确认输出看起来正确,数值在预期范围内。
  3. 验证通过后再运行完整测试套件。

在迭代基准测试设计时,使用 --job Short,待确认无误后再切换到默认设置以获取最终数据。

评论

支持 Markdown