BenchmarkDotNet 泛型基准测试实战:使用 [GenericTypeArguments] 为开放泛型 Benchmark 类注入类型参数
2026/9/23 10:17:53 网站建设 项目流程

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 IntroGenericTypeArguments<T> { [Benchmark] public T Create() => Activator.CreateInstance<T>(); } }

该示例的核心思想非常简洁:

  • 定义了一个开放泛型基准类IntroGenericTypeArguments<T>,其中含有一个[Benchmark]标记的方法Create()
  • Create()通过Activator.CreateInstance<T>()创建类型T的实例并返回,用于测量"通过反射创建不同类型实例"的开销;
  • 类上通过两个[GenericTypeArguments]特性分别指定typeof(int)typeof(char),表示将T分别闭合为intchar各跑一轮基准。

运行后,BenchmarkDotNet 会为该类生成两个独立的基准用例(Benchmark Case):IntroGenericTypeArguments<int>.Create()IntroGenericTypeArguments<char>.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) { ... } // 三类型实参 }

从定义可以看出三个要点:

  1. 目标对象AttributeTargets.Class,只能标注在类上(Benchmark 类本身);
  2. 可重复AllowMultiple = true,允许在同一类上叠加多个[GenericTypeArguments]特性,每个特性对应一组类型实参,从而生成多组闭合后的基准;
  3. 参数个数:构造函数提供 1 个、2 个、3 个Type三种重载,分别对应泛型类型定义中T<T1, T2><T1, T2, T3>三种常见情况。类型实参数必须与类的泛型参数个数一致,否则无法成功闭合(详见下文分析器校验部分)。

此外,特性构造函数的Type参数均标注了[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)],这是为 Native AOT / 裁剪场景保留类型元数据,确保泛型实参类型在发布裁剪后仍能被反射完整访问。

底层原理:GenericBenchmarksBuilder 如何闭合泛型类型

BenchmarkDotNet 会在发现基准类阶段,将带[GenericTypeArguments]的开放泛型类型转换为可运行的具体闭合类型。核心实现位于 src/BenchmarkDotNet/Helpers/GenericBenchmarksBuilder.cs:

internal static Type[] GetRunnableBenchmarks(IEnumerable<Type> 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).OfType<GenericTypeArgumentsAttribute>() .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, IEnumerable<Type[]> 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; } }

整个流程可以拆解为四步:

  1. 过滤可运行类型ContainsRunnableBenchmarks()先排除抽象类,再结合"是否是开放泛型"进行判断(详见下文);
  2. 读取特性:通过反射读取类上的所有GenericTypeArgumentsAttribute,得到一组组类型实参数组;
  3. 逐一闭合:对每组类型实参调用type.MakeGenericType(genericArg),把开放泛型类型定义(如IntroGenericTypeArguments<>)实例化为闭合类型(如IntroGenericTypeArguments<int>);
  4. 失败兜底MakeGenericType在实参个数不匹配或实参约束不满足时会抛出ArgumentExceptionTryMakeGenericType将其捕获并标记isSuccess = false,后续Where(x => x.isSuccess)会直接丢弃该组失败类型,不会让整个运行崩溃。

同时,src/BenchmarkDotNet/Extensions/ReflectionExtensions.cs 中的ContainsRunnableBenchmarksIsRunnableGenericType定义了"什么样的泛型类是可达的":

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).OfType<GenericTypeArgumentsAttribute>().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 OneArgGenericBenchmark<T> { [Benchmark] public T CreateT() => Activator.CreateInstance<T>(); }

对应测试断言:GetRunnableBenchmarks返回恰好 2 个类型,且分别为OneArgGenericBenchmark<int>OneArgGenericBenchmark<char>

双类型实参

[GenericTypeArguments(typeof(int), typeof(char))] [GenericTypeArguments(typeof(char), typeof(string))] public class TwoArgGenericBenchmark<T1, T2> { [Benchmark] public T1 CreateT1() => Activator.CreateInstance<T1>(); [Benchmark] public T2 CreateT2() => Activator.CreateInstance<T2>(); }

三类型实参

[GenericTypeArguments(typeof(int), typeof(char), typeof(string))] [GenericTypeArguments(typeof(char), typeof(string), typeof(byte))] public class ThreeArgGenericBenchmark<T1, T2, T3> { [Benchmark] public T1 CreateT1() => Activator.CreateInstance<T1>(); [Benchmark] public T2 CreateT2() => Activator.CreateInstance<T2>(); [Benchmark] public T3 CreateT3() => Activator.CreateInstance<T3>(); }

由测试可以确认:每组[GenericTypeArguments]是一组独立的类型实参组合,N 个特性就会生成 N 个闭合类型,每个闭合类型各自作为独立基准参与测量,互不干扰。

分析器约束:编写时的编译期保护

BenchmarkDotNet 提供了配套的 Roslyn 分析器(src/BenchmarkDotNet.Analyzers),其中的 BenchmarkClassAnalyzer.cs 定义了与泛型类型实参相关的两条诊断规则,在编译期即可拦截错误用法:

  1. ClassWithGenericTypeArgumentsAttributeMustBeGenericRule[GenericTypeArguments]只能标注在泛型类上。若把该特性放在非泛型类上,分析器会直接报错,因为类型实参无处可闭;
  2. GenericTypeArgumentsAttributeMustHaveMatchingTypeParameterCountRule:特性提供的类型实参数必须与类声明的泛型参数个数一致。例如类声明为<T1, T2>却只传了一个typeof(int),编译期即被标记为错误(若在无分析器的环境下运行,这一错误会落入GenericBenchmarksBuilder.TryMakeGenericTypeArgumentException捕获分支,对应的闭合类型会被静默丢弃)。

这两条规则进一步印证了特性的使用契约:泛型类 + 匹配数量的类型实参 + 公共无参构造函数三者缺一不可。

运行与验证

在仓库根目录下,可通过以下方式运行该示例:

dotnet run -c Release --project samples/BenchmarkDotNet.Samples --filter *IntroGenericTypeArguments*

运行结束后,输出结果中会看到两组独立的结果行,分别对应IntroGenericTypeArguments<int>IntroGenericTypeArguments<char>两个闭合类型的Create基准,各自包含 Mean、Error、StdDev 等统计列,可用于比较同一段泛型代码在不同类型实参下的性能差异。

若想在不启动完整基准运行的情况下验证"泛型闭合"逻辑本身,可直接运行官方单元测试:

dotnet test tests/BenchmarkDotNet.Tests --filter FullyQualifiedName~GenericBuilderTests

GenericBuilderTests中的TestBuildGenericWithOneArgumentTestBuildGenericWithTwoArgumentsTestBuildGenericWithThreeArguments三个测试用例,分别验证了 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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询