免费获取学习方案
ARTICLE DETAIL

资讯详情

深耕编程基础知识与建站技术分享的一线实战洞察。

BenchmarkDotNet 泛型基准测试实战:使用 [GenericTypeArguments] 为开放泛型 Benchmark 类注入类型参数

BenchmarkDotNet 泛型基准测试实战:使用 [GenericTypeArguments] 为开放泛型 Benchmark 类注入类型参数 BenchmarkDotNet 泛型基准测试实战使用 [GenericTypeArguments] 为开放泛型 Benchmark 类注入类型参数【免费下载链接】BenchmarkDotNetPowerful .NET library for benchmarking项目地址: https://gitcode.com/gh_mirrors/be/BenchmarkDotNet泛型 Benchmark 类是 C# 基准测试中常见的进阶场景但 BenchmarkDotNet 默认无法直接运行未闭合的泛型类型open generic type。本文以官方示例IntroGenericTypeArguments为主线讲解如何通过[GenericTypeArguments]特性为泛型 Benchmark 类指定一组或多组类型实参使同一套基准逻辑可以针对不同类型分别测量并深入仓库源码剖析其底层闭合机制、可重复特性用法以及内置 Roslyn 分析器的约束校验帮助读者写出可复制的多类型泛型基准测试。示例概述IntroGenericTypeArguments官方示例位于 samples/BenchmarkDotNet.Samples/IntroGenericTypeArguments.cs完整源码如下using BenchmarkDotNet.Attributes; namespace BenchmarkDotNet.Samples { [GenericTypeArguments(typeof(int))] [GenericTypeArguments(typeof(char))] public class IntroGenericTypeArgumentsT { [Benchmark] public T Create() Activator.CreateInstanceT(); } }该示例的核心思想非常简洁定义了一个开放泛型基准类IntroGenericTypeArgumentsT其中含有一个[Benchmark]标记的方法Create()Create()通过Activator.CreateInstanceT()创建类型T的实例并返回用于测量通过反射创建不同类型实例的开销类上通过两个[GenericTypeArguments]特性分别指定typeof(int)与typeof(char)表示将T分别闭合为int和char各跑一轮基准。运行后BenchmarkDotNet 会为该类生成两个独立的基准用例Benchmark CaseIntroGenericTypeArgumentsint.Create()与IntroGenericTypeArgumentschar.Create()分别输出各自的时间/内存统计结果。该示例对应的官方文档为 docs/articles/samples/IntroGenericTypeArguments.md其中以!code-csharp指令直接内嵌了上述源码并给出了指向该示例的永久链接BenchmarkDotNet.Samples.IntroGenericTypeArguments。特性定义GenericTypeArgumentsAttribute 的三种构造形式[GenericTypeArguments]特性的定义位于 src/BenchmarkDotNet.Annotations/Attributes/GenericTypeArgumentsAttribute.cs其关键声明如下[AttributeUsage(AttributeTargets.Class, AllowMultiple true)] public class GenericTypeArgumentsAttribute : Attribute { public Type[] GenericTypeArguments { get; } public GenericTypeArgumentsAttribute(Type type) { ... } // 单类型实参 public GenericTypeArgumentsAttribute(Type type1, Type type2) { ... } // 双类型实参 public GenericTypeArgumentsAttribute(Type type1, Type type2, Type type3) { ... } // 三类型实参 }从定义可以看出三个要点目标对象AttributeTargets.Class只能标注在类上Benchmark 类本身可重复AllowMultiple true允许在同一类上叠加多个[GenericTypeArguments]特性每个特性对应一组类型实参从而生成多组闭合后的基准参数个数构造函数提供 1 个、2 个、3 个Type三种重载分别对应泛型类型定义中T、T1, T2、T1, T2, T3三种常见情况。类型实参数必须与类的泛型参数个数一致否则无法成功闭合详见下文分析器校验部分。此外特性构造函数的Type参数均标注了[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)]这是为 Native AOT / 裁剪场景保留类型元数据确保泛型实参类型在发布裁剪后仍能被反射完整访问。底层原理GenericBenchmarksBuilder 如何闭合泛型类型BenchmarkDotNet 会在发现基准类阶段将带[GenericTypeArguments]的开放泛型类型转换为可运行的具体闭合类型。核心实现位于 src/BenchmarkDotNet/Helpers/GenericBenchmarksBuilder.csinternal static Type[] GetRunnableBenchmarks(IEnumerableType types) types.Where(type type.ContainsRunnableBenchmarks()) .SelectMany(BuildGenericsIfNeeded) .Where(x x.isSuccess) .Select(x x.result) .ToArray(); internal static IEnumerable(bool isSuccess, Type result) BuildGenericsIfNeeded(Type type) { var typeArguments type.GetCustomAttributes(true).OfTypeGenericTypeArgumentsAttribute() .Select(x x.GenericTypeArguments) .ToArray(); if (typeArguments.Any()) return BuildGenericTypes(type, typeArguments); return [(true, type)]; } private static IEnumerable(bool isSuccess, Type result) BuildGenericTypes(Type type, IEnumerableType[] typeArguments) typeArguments.Select(genericArg (type.TryMakeGenericType(genericArg, out var builtType), builtType)); private static bool TryMakeGenericType(this Type type, Type[] typeArguments, out Type result) { try { result type.MakeGenericType(typeArguments); return true; } catch (ArgumentException) { result type; return false; } }整个流程可以拆解为四步过滤可运行类型ContainsRunnableBenchmarks()先排除抽象类再结合是否是开放泛型进行判断详见下文读取特性通过反射读取类上的所有GenericTypeArgumentsAttribute得到一组组类型实参数组逐一闭合对每组类型实参调用type.MakeGenericType(genericArg)把开放泛型类型定义如IntroGenericTypeArguments实例化为闭合类型如IntroGenericTypeArgumentsint失败兜底MakeGenericType在实参个数不匹配或实参约束不满足时会抛出ArgumentExceptionTryMakeGenericType将其捕获并标记isSuccess false后续Where(x x.isSuccess)会直接丢弃该组失败类型不会让整个运行崩溃。同时src/BenchmarkDotNet/Extensions/ReflectionExtensions.cs 中的ContainsRunnableBenchmarks与IsRunnableGenericType定义了什么样的泛型类是可达的internal static bool ContainsRunnableBenchmarks(this Type type) { var typeInfo type.GetTypeInfo(); if (typeInfo.IsAbstract || typeInfo.IsGenericType !IsRunnableGenericType(typeInfo)) return false; return typeInfo.GetBenchmarks().Any(); } private static bool IsRunnableGenericType(TypeInfo typeInfo) (!typeInfo.IsGenericTypeDefinition || typeInfo.GenericTypeArguments.Any() || typeInfo.GetCustomAttributes(true).OfTypeGenericTypeArgumentsAttribute().Any()) typeInfo.DeclaredConstructors.Any(ctor ctor.IsPublic ctor.GetParameters().Length 0);关键结论有两点一个未闭合的泛型类型定义IsGenericTypeDefinition为 true 且无已闭合实参只有在携带[GenericTypeArguments]特性时才会被视为可运行否则会被直接过滤掉——这解释了为什么示例类必须标注该特性无论是否泛型类都必须具备公共无参构造函数public且参数个数为 0因为 BenchmarkDotNet 需要以无参方式实例化基准类来执行每个[Benchmark]方法。多类型实参覆盖一参、两参、三参泛型GenericTypeArgumentsAttribute的三种构造函数覆盖了最常见的泛型元数arity。官方测试 tests/BenchmarkDotNet.Tests/GenericBuilderTests.cs 对这三种情况均有覆盖单类型实参与 IntroGenericTypeArguments 示例完全一致[GenericTypeArguments(typeof(int))] [GenericTypeArguments(typeof(char))] public class OneArgGenericBenchmarkT { [Benchmark] public T CreateT() Activator.CreateInstanceT(); }对应测试断言GetRunnableBenchmarks返回恰好 2 个类型且分别为OneArgGenericBenchmarkint与OneArgGenericBenchmarkchar。双类型实参[GenericTypeArguments(typeof(int), typeof(char))] [GenericTypeArguments(typeof(char), typeof(string))] public class TwoArgGenericBenchmarkT1, T2 { [Benchmark] public T1 CreateT1() Activator.CreateInstanceT1(); [Benchmark] public T2 CreateT2() Activator.CreateInstanceT2(); }三类型实参[GenericTypeArguments(typeof(int), typeof(char), typeof(string))] [GenericTypeArguments(typeof(char), typeof(string), typeof(byte))] public class ThreeArgGenericBenchmarkT1, T2, T3 { [Benchmark] public T1 CreateT1() Activator.CreateInstanceT1(); [Benchmark] public T2 CreateT2() Activator.CreateInstanceT2(); [Benchmark] public T3 CreateT3() Activator.CreateInstanceT3(); }由测试可以确认每组[GenericTypeArguments]是一组独立的类型实参组合N 个特性就会生成 N 个闭合类型每个闭合类型各自作为独立基准参与测量互不干扰。分析器约束编写时的编译期保护BenchmarkDotNet 提供了配套的 Roslyn 分析器src/BenchmarkDotNet.Analyzers其中的 BenchmarkClassAnalyzer.cs 定义了与泛型类型实参相关的两条诊断规则在编译期即可拦截错误用法ClassWithGenericTypeArgumentsAttributeMustBeGenericRule[GenericTypeArguments]只能标注在泛型类上。若把该特性放在非泛型类上分析器会直接报错因为类型实参无处可闭GenericTypeArgumentsAttributeMustHaveMatchingTypeParameterCountRule特性提供的类型实参数必须与类声明的泛型参数个数一致。例如类声明为T1, T2却只传了一个typeof(int)编译期即被标记为错误若在无分析器的环境下运行这一错误会落入GenericBenchmarksBuilder.TryMakeGenericType的ArgumentException捕获分支对应的闭合类型会被静默丢弃。这两条规则进一步印证了特性的使用契约泛型类 匹配数量的类型实参 公共无参构造函数三者缺一不可。运行与验证在仓库根目录下可通过以下方式运行该示例dotnet run -c Release --project samples/BenchmarkDotNet.Samples --filter *IntroGenericTypeArguments*运行结束后输出结果中会看到两组独立的结果行分别对应IntroGenericTypeArgumentsint与IntroGenericTypeArgumentschar两个闭合类型的Create基准各自包含 Mean、Error、StdDev 等统计列可用于比较同一段泛型代码在不同类型实参下的性能差异。若想在不启动完整基准运行的情况下验证泛型闭合逻辑本身可直接运行官方单元测试dotnet test tests/BenchmarkDotNet.Tests --filter FullyQualifiedName~GenericBuilderTestsGenericBuilderTests中的TestBuildGenericWithOneArgument、TestBuildGenericWithTwoArguments、TestBuildGenericWithThreeArguments三个测试用例分别验证了 1 参、2 参、3 参泛型在[GenericTypeArguments]驱动下的闭合结果数量与具体类型是与示例源码相互印证的底层行为测试。小结IntroGenericTypeArguments展示了 BenchmarkDotNet 中一套泛型基准逻辑、多组类型实参的标准做法用[GenericTypeArguments]可叠加为泛型 Benchmark 类声明一组或多组类型实参底层由 GenericBenchmarksBuilder 通过反射读取特性并调用MakeGenericType逐一闭合失败组合被安全跳过闭合后的每个类型成为一个独立基准用例与普通 Benchmark 类同等参与预热、迭代与统计编译期由 BenchmarkClassAnalyzer 的规则约束非泛型类不可用与实参个数必须匹配两类错误用法。对于需要在不同泛型实参如int/long/string/自定义结构体下重复测量同一算法实现的场景该特性是保持代码 DRY 且保证测量独立性的推荐方案。【免费下载链接】BenchmarkDotNetPowerful .NET library for benchmarking项目地址: https://gitcode.com/gh_mirrors/be/BenchmarkDotNet创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表