ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

BenchmarkDotNet 实战:用 NuGet 包多版本对比基准测试(IntroNuGet 示例深度解析)

BenchmarkDotNet 实战:用 NuGet 包多版本对比基准测试(IntroNuGet 示例深度解析) BenchmarkDotNet 实战用 NuGet 包多版本对比基准测试IntroNuGet 示例深度解析【免费下载链接】BenchmarkDotNetPowerful .NET library for benchmarking项目地址: https://gitcode.com/gh_mirrors/be/BenchmarkDotNet本文基于 BenchmarkDotNet 仓库中的 IntroNuGet 示例文档 与配套示例源码编写。当你在升级某个 NuGet 依赖如 Newtonsoft.Json时往往想知道新版本到底快了多少、有没有性能回退。BenchmarkDotNet 提供了一种优雅的方案借助 MSBuild 属性为每个 Job 指定不同的 NuGet 包版本让同一个基准方法在多个包版本下分别编译、运行并对比结果。读完本文你将掌握如何改造自己的 csproj、如何在配置中按版本生成多个 Job、以及这一机制在 BenchmarkDotNet 内部的实现原理涉及工具链与参数传递链路从而在自己的项目里直接复刻同包多版本横评能力。一、示例要解决的问题同包多版本对比在日常性能工程中一个非常典型的需求是在 API 不发生破坏性变更的前提下比较同一个 NuGet 包的多个版本之间的性能差异。例如升级依赖前先评估v13.0.4 相比 v13.0.1 是否值得升级验证某个版本是否引入性能回退为团队决策锁定哪个版本提供可量化的依据。BenchmarkDotNet 的IntroNuGet示例正是针对这一场景设计的。其核心思路写在示例文档的开头You can set specific versions of NuGet dependencies for each job using MsBuild properties in your csproj. It allows comparing different versions of the same package (if there are no breaking changes in API).即通过在 csproj 中定义 MSBuild 属性来为每个 Job 指定具体的 NuGet 依赖版本从而在同一个基准程序内对比同一包的不同版本前提是这些版本与基准代码源码兼容不存在 API 破坏性变更。这里有一个关键约束需要提前说明该机制仅适用于基于项目文件CsProj的工具链即 BenchmarkDotNet 在独立进程中通过dotnet build/publish重新编译基准项目的方式来运行基准。示例源码 IntroNuGet.cs 的 XML 注释也明确标注了这一点/// remarks /// Only supported with CsProj toolchains. /// /remarks二、第一步改造 csproj把包版本参数化要让每个 Job 使用不同的包版本首先必须让包版本成为 MSBuild 的一个可注入属性而不是写死的字面量。示例在 BenchmarkDotNet.Samples.csproj 中给出了实际可运行的配置PropertyGroup !-- Use 13.0.1 as default package version for IntroNuGet -- NewtonsoftJsonVersion Condition$(NewtonsoftJsonVersion) 13.0.1/NewtonsoftJsonVersion /PropertyGroup ItemGroup PackageReference IncludeNewtonsoft.Json Version[$(NewtonsoftJsonVersion)] / /ItemGroup解读这段配置的三个要点属性默认值Condition$(NewtonsoftJsonVersion) 表示当命令行没有传入该属性时使用 13.0.1 作为默认版本。这保证了不传参数时项目依然可以正常构建。版本区间语法Version[$(NewtonsoftJsonVersion)]中的方括号[ ]是 NuGet 的精确版本区间写法表示只允许恰好等于该版本的包避免解析到更高的版本导致对比失真。属性值通过$(NewtonsoftJsonVersion)引用。与 BenchmarkDotNet 自动生成的临时项目的差异当 BenchmarkDotNet 为某个 Job 构建基准时它会生成一个临时项目并注入你通过 Job 配置传入的 MSBuild 参数详见后文第三节临时项目会继承这个参数化版本从而解析到指定版本的包。示例源码 IntroNuGet.cs 的注释里还给出了与 csproj 完全一致的模板式写法便于你把这段配置复制到自己项目的 csproj 中// Setup your csproj like this: /* PropertyGroup !-- Use 13.0.1 as default package version if not specified -- NewtonsoftJsonVersion Condition$(NewtonsoftJsonVersion) 13.0.1/NewtonsoftJsonVersion /PropertyGroup ItemGroup PackageReference IncludeNewtonsoft.Json Version[$(NewtonsoftJsonVersion)] / /ItemGroup */三、第二步在配置中为每个 Job 注入不同版本csproj 准备好之后剩下的工作全部在基准类的配置Config中完成。示例 IntroNuGet.cs 定义了一个私有Config类继承自ManualConfigprivate class Config : ManualConfig { public Config() { string[] targetVersions [ 13.0.1, 13.0.2, 13.0.3, 13.0.4, ]; foreach (var version in targetVersions) { AddJob(Job.MediumRun .WithMsBuildArguments($/p:NewtonsoftJsonVersion{version}) .WithId($v{version}) ); } } }这段代码的核心逻辑枚举目标版本把要对比的版本号放在一个字符串数组里未来想增加对比版本只需往数组里加一项。为每个版本生成一个 Jobforeach循环为每个版本调用一次AddJob最终注册 4 个 Job。WithMsBuildArguments这是关键 API它把/p:NewtonsoftJsonVersion13.0.x这样的 MSBuild 属性参数挂到 Job 上。这样当 BenchmarkDotNet 为这个 Job 构建基准程序时就会用指定的版本号去解析Newtonsoft.Json包。WithId给每个 Job 起一个可读的名字v13.0.1、v13.0.2……这个名字会直接出现在基准结果表格的Job列中见第四节输出表也用于区分产物目录与日志。随后基准类通过[Config(typeof(Config))]挂载该配置[Config(typeof(Config))] public class IntroNuGet而基准方法本身完全不需要感知现在跑的是哪个版本因为不同版本的程序是分别编译、分别运行的[Benchmark] public void SerializeAnonymousObject() { JsonConvert.SerializeObject( new { hello world, price 1.99, now DateTime.UtcNow }); }这里选择JsonConvert.SerializeObject匿名对象序列化作为基准方法是因为它在 Newtonsoft.Json 的各个 13.0.x 小版本之间保持 API 与行为稳定非常适合用来验证版本间性能差异。关于 Job.MediumRunJob.MediumRun是 BenchmarkDotNet 内置的预设 Job 之一代表中等时长的测量配置。在 Job.cs 中MediumRun的定义大致对应预热Warmup约 100 次迭代正式测量Target约 15 次迭代单次迭代时长为 100 毫秒。相比ShortRun更快但精度低与LongRun更慢但精度高MediumRun是日常对比基准中兼顾精度与耗时的常用选择。你也可以按需改用Job.Default或自定义运行策略参考 choosing-run-strategy.md。四、运行与输出一份表格看清版本差异在samples目录下编译运行 BenchmarkDotNet.Samples 项目从基准列表中选择IntroNuGet即可执行。示例文档给出了该示例在示例作者环境中的一次真实运行结果MethodJobArgumentsMeanErrorStdDevSerializeAnonymousObjectv13.0.1/p:NewtonsoftJsonVersion13.0.1652.7 ns10.68 ns15.98 nsSerializeAnonymousObjectv13.0.2/p:NewtonsoftJsonVersion13.0.2654.0 ns8.62 ns12.89 nsSerializeAnonymousObjectv13.0.3/p:NewtonsoftJsonVersion13.0.3678.6 ns17.62 ns26.38 nsSerializeAnonymousObjectv13.0.4/p:NewtonsoftJsonVersion13.0.4637.2 ns16.95 ns24.84 ns输出表解读Method 列基准方法名SerializeAnonymousObject。Job 列显示v13.0.1、v13.0.2等正是WithId指定的 Job 标识。Arguments 列显示该 Job 实际注入的 MSBuild 参数/p:NewtonsoftJsonVersion13.0.x与WithMsBuildArguments传入的内容一一对应。Mean / Error / StdDev 列平均值、误差99.9% 置信区间半宽与标准差。从表中可以看到 13.0.4 的均值637.2 ns与 13.0.1652.7 ns之间仅有约 2% 的差异且各组置信区间有重叠——单凭该示例数据4 个版本在序列化匿名对象这个场景下没有统计上显著的性能差异。这正是用数据说话的体现对比结果不靠猜测而是由误差范围内的均值直接呈现。需要说明的是上述数值是示例文档记录的某一次运行结果不同机器、不同运行时环境下数值会有波动你在自己环境里运行得到的绝对数值会不同但表格结构与对比方法完全一致。若想进一步判断差异是否显著可以给 Config 加上[StatisticalTestColumn]或使用 IntroStatisticalTesting.md 中的统计检验列。五、底层原理MSBuild 参数是如何从 Job 传递到构建命令的了解完用法后再深入一层看 BenchmarkDotNet 是如何把WithMsBuildArguments的字符串一路送到dotnet build命令行中的。整个链路分三步1. 参数被封装为 MsBuildArgument 对象JobExtensions.cs 中WithMsBuildArguments的实现非常简单——把每个字符串包装成一个MsBuildArgument并存入 Job 的Infrastructure.Arguments集合public static Job WithArguments(this Job job, IReadOnlyListArgument arguments) job.WithCore(j j.Infrastructure.Arguments arguments); public static Job WithMsBuildArguments(this Job job, params string[] msBuildArguments) job.WithArguments([.. msBuildArguments.Select(a new MsBuildArgument(a))]);MsBuildArgument定义于 Argument.cs注释明确说明其语义Argument passed to dotnet cli when restoring and building the project。它同时提供了对 MSBuild 特殊字符%、$、、( )、;、?、*等的转义支持EscapeSpecialCharacters方法遵循 MSBuild 的转义规范并且派生类MsBuildPropertyArgument.cs进一步封装了/p:namevalue形式属性的构造会自动处理多值以;连接与特殊字符转义。也就是说除了手写/p:NewtonsoftJsonVersion13.0.1字符串你还可以用更安全的new MsBuildProperty(NewtonsoftJsonVersion, 13.0.1)这类方式构造参数。2. 构建命令组装时提取参数在基于 .NET CLI 的工具链中DotNetCliCommand.cs 负责把基准项目编译为可执行程序。它的restore第 150 行、build第 163 行、publish第 177 行三条命令的组装过程中都调用了同一个方法GetCustomMsBuildArguments把 Job 上的自定义 MSBuild 参数追加到命令行尾部。3. 参数拼接与命令行注入GetCustomMsBuildArguments的实现位于 DotNetCliCommand.csprivate static string GetCustomMsBuildArguments(BenchmarkCase benchmarkCase, IResolver resolver) { if (!benchmarkCase.Job.HasValue(InfrastructureMode.ArgumentsCharacteristic)) return ; var msBuildArguments benchmarkCase.Job.ResolveValue(InfrastructureMode.ArgumentsCharacteristic, resolver)!.OfTypeMsBuildArgument(); return string.Join( , msBuildArguments.Select(arg arg.TextRepresentation)); }其逻辑为先检查当前 Job 是否显式设置了Arguments特性InfrastructureMode.ArgumentsCharacteristic如果没设置则返回空串不干扰正常构建否则取出其中所有MsBuildArgument类型的参数用空格拼接后作为额外的 MSBuild 参数注入。于是/p:NewtonsoftJsonVersion13.0.4就出现在最终的dotnet build/dotnet publish命令行中NuGet 还原restore阶段据此解析出指定版本的 Newtonsoft.Json 程序集。从源码结构还可以推断出两点设计意图参数的Job 级定位参数是挂在BenchmarkCase.Job上的通过RepresentativeBenchmarkCase代表整个构建分区取参因此不同 Job 可以携带完全不同的 MSBuild 参数这正是每版本一个 Job能成立的根本原因。restore/build/publish 全链路生效GetCustomMsBuildArguments在三处命令中都被调用说明参数不仅影响编译还影响 NuGet 还原——这也是Version[...]精确版本区间能正确解析的前提。六、使用前提、限制与最佳实践综合示例文档、示例源码与工具链实现使用该方案时有几点需要特别留意仅限 CsProj 工具链该机制依赖 BenchmarkDotNet 用 MSBuild/dotnet CLI 重新编译基准项目。InProcess 等其他工具链不会经历还原 编译流程自然无法按版本切换依赖。版本之间必须源码兼容示例文档明确强调这一点if there are no breaking changes in API。如果某个版本改了 API基准代码可能在该版本下编译失败整个 Job 也会失败。对比前最好先确认目标版本与你的基准代码兼容。参数会出现在输出中从第四节输出表的Arguments列可以看到注入的 MSBuild 参数会原样展示在基准结果里。这不只是展示更是可复现性的体现——任何人看到结果都能知道该行数据对应的确切包版本。保持其他变量一致为了让对比只反映包版本这一个变量建议所有 Job 使用相同的运行模式本示例统一使用Job.MediumRun、相同的基准方法、相同的目标框架。若要切换框架可参考 customizing-runtime.md。csproj 属性默认值很重要Condition$(NewtonsoftJsonVersion) 的兜底逻辑不仅保证不传参数时可正常构建也保证了项目在 IDE 中不经 BenchmarkDotNet仍能编译运行。七、延伸阅读示例源码IntroNuGet.cs、参数化 csproj 配置BenchmarkDotNet.Samples.csproj示例文档原文IntroNuGet.mdJob 配置 APIJobExtensions.cs、参数类型定义Argument.cs参数注入实现DotNetCliCommand.cs运行策略选择choosing-run-strategy.md统计显著性检验IntroStatisticalTesting.md参数化Params与多 Job 的其他用法IntroParams.md、IntroArguments.md【免费下载链接】BenchmarkDotNetPowerful .NET library for benchmarking项目地址: https://gitcode.com/gh_mirrors/be/BenchmarkDotNet创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表