关于 microbenchmarking
microbenchmarking 是指导使用 BenchmarkDotNet 编写和运行微基准测试的技能,适用于需要比较实现、运行时或配置性能的 .NET 开发者。它强调按测量目标设计用例,并先查阅相关参考资料以遵循当前行为。
主要功能
- 01规划基准测试的比较维度
- 02选择参数化和输入数据方式
- 03配置 BenchmarkDotNet 入口与选项
- 04控制测试用例数量和运行耗时
- 05校验结果并阅读报告
适用场景
- 01比较不同实现的性能
- 02调查特定性能问题
- 03评估代码变更的性能影响
- 04为开发反馈创建临时基准测试
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ dotnet/ skills --skill microbenchmarking安装后在 Claude Code 中输入/microbenchmarking确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/bdn-internals-and-tuning.mdcomparison-strategies.mddiagnosers-and-exporters.mdproject-setup-and-running.mdwriting-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,了解每种策略的正确做法。
用例和基准测试生命周期
开发者编写基准测试有四种不同的原因,每种原因都会影响基准测试的设计方式和存放位置:
-
覆盖率套件:编写基准测试,尽可能覆盖真实世界的使用模式,以便捕获影响大多数用户的回归问题。这些基准测试是永久性的——它们属于项目的基准测试套件,遵循项目的惯例(目录结构、基类、命名),并提交到代码库中。
-
问题调查:有人报告了特定的性能问题。编写基准测试以复现并诊断该问题。这些基准测试的作用范围仅限于当前任务——它们会在调查期间持续存在(复现 → 隔离 → 验证修复),但不会成为永久套件的一部分。
-
变更验证:开发者有一个 PR 或变更,希望在合并前了解其性能特征。这些基准测试的作用范围仅限于当前任务——它们会在评审周期内持续存在,但不会提交到代码库中。
-
开发反馈:开发者正在处理一项任务,希望使用基准测试评估不同方案并尽早获取信息。这些基准测试的作用范围仅限于当前任务,且会在任务结束后丢弃——它们会在开发过程中持续存在,但会在做出决定后删除。
对于用例 1,请遵循现有基准测试项目的惯例,将基准测试添加到项目中。对于用例 2–4,请在工作目录中创建独立项目,在任务期间保留,但要明确表明它不属于永久代码库。
对于覆盖率套件基准测试,请从实际调用方的角度进行设计——哪些代码模式会使用此 API,他们会传入哪些输入,以及哪些性能特征对他们至关重要。每个永久基准测试都应通过其与真实世界的相关性,证明其维护成本是合理的。对于临时基准测试,应有意控制测试用例数量——每增加一个测试用例,都会增加实际耗时(请参阅成本意识)。
成本意识
使用默认设置时,每个基准测试用例(一个方法 × 一种参数组合 × 一个作业)需要 15–25 秒。[Params] 会生成笛卡尔积:两个 [Params] 分别有 3 个和 4 个值,应用于 5 个方法时,共有 60 个用例,耗时约 20 分钟。多个作业会进一步增加总耗时。运行前,请估算用例总数,并根据情况选择作业预设:
| 预设 | 每个用例耗时 | 适用场景 |
|---|---|---|
--job Dry | <1s | 验证正确性——确认可以编译和执行,但不进行测量 |
--job Short | 5–8s | 在开发或调查期间快速测量 |
| (默认) | 15–25s | 为覆盖率套件进行最终测量 |
--job Medium | 33–52s | 在结果至关重要时提高可信度 |
--job Long | 3–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:验证并运行
在提交长时间运行前进行验证:
- 先使用
--job Dry运行,以便发现编译错误和运行时异常,避免在测量上浪费时间。 - 使用默认设置运行一个有代表性的案例,确认输出看起来正确,数值在预期范围内。
- 验证通过后再运行完整测试套件。
在迭代基准测试设计时,使用 --job Short,待确认无误后再切换到默认设置以获取最终数据。