精准的性能数据是优化.NET应用的前提。手工用Stopwatch掐表虽然简单,但JIT预热不充分、垃圾回收随机触发、线程调度抖动都会让单次计时充满噪音。BenchmarkDotNet把预热、多次迭代、离群值剔除和统计检验打包成一条流水线,输出平均值、误差范围和内存分配等关键指标。接下来先完成一次最小可运行的基准测试。

安装与最小示例
要开始使用BenchmarkDotNet,首先需要创建一个控制台项目,然后通过NuGet安装BenchmarkDotNet包。建议使用.NET CLI执行以下命令:
dotnet new console -n BenchDemo cd BenchDemo dotnet add package BenchmarkDotNet
安装完成后,在Program.cs中编写基准测试类。基准测试类通常是一个普通类,里面包含一个或多个用[Benchmark]特性标记的方法。BenchmarkDotNet会识别这些方法,并对每个方法执行预热和多次测量。下面以字符串拼接为例,比较StringBuilder与直接使用加号拼接的性能差异。
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
using System.Linq;
public class StringBenchmark
{
private readonly string[] values = Enumerable.Range(1, 1000).Select(i => i.ToString()).ToArray();
[Benchmark]
public string JoinWithStringBuilder()
{
var sb = new System.Text.StringBuilder();
foreach (var v in values)
{
sb.Append(v);
}
return sb.ToString();
}
[Benchmark]
public string JoinWithStringConcat()
{
var result = string.Empty;
foreach (var v in values)
{
result += v;
}
return result;
}
}
public class Program
{
public static void Main(string[] args)
{
BenchmarkRunner.Run<StringBenchmark>();
}
}
代码中的[Benchmark]特性告诉BenchmarkDotNet该方法需要被测量。两个方法都遍历同一个字符串数组,但JoinWithStringConcat每次循环都会创建新的字符串对象,理论上会产生大量临时分配,性能较差。Main方法中调用BenchmarkRunner.Run<StringBenchmark>()会启动基准测试流程。注意必须使用Release配置运行,Debug配置下JIT优化被关闭,测出的数据没有参考价值。
运行命令为dotnet run -c Release。首次运行可能需要较长时间,因为BenchmarkDotNet会执行预热、多次迭代以及统计计算。最终会在控制台输出一张包含平均值、误差、标准差等信息的表格,并生成HTML报告到BenchmarkDotNet.Artifacts目录。
理解基准测试报告与核心指标
运行完成后,控制台会显示类似下面的摘要表格。每行对应一个基准方法,列包含Mean、Error、StdDev、Median、Allocated等。理解这些指标是正确解读性能数据的基础。Mean表示单次操作的平均耗时,单位通常是纳秒或微秒;Error和StdDev反映多次测量的波动范围,误差越小说明结果越稳定;Median是中位数,能够减少异常值对平均值的影响。
Allocated列显示每次操作分配的托管内存字节数。对于频繁执行的方法,内存分配会直接触发垃圾回收,导致吞吐量下降。如果启用[MemoryDiagnoser],还会看到Gen0、Gen1、Gen2列,分别代表各代垃圾回收的触发次数。比如上面的字符串拼接例子,StringBuilder版本分配较少且没有频繁GC,而加号拼接每次循环都会产生新字符串,Allocated和Gen0数字会明显更高。
比较两个基准方法时,不能只看Mean的绝对差值是否大于0。BenchmarkDotNet会进行统计检验,判断差异是否显著。报告中可能包含Ratio列,如果某个方法标记为Baseline,其他方法会显示相对于基线的倍数,例如1.00表示与基线相同,2.35表示慢2.35倍。在优化前后对比时,建议把旧实现标记为[Benchmark(Baseline = true)],这样新实现的性能提升或回退一目了然。
参数化基准测试与内存诊断
真实场景中,算法性能往往依赖输入规模。固定一千个字符串的对比虽然直观,但无法说明在更大或更小数据量下是否成立。BenchmarkDotNet的[Params]特性允许为同一个基准方法提供多个参数值,每次使用不同参数分别运行测量。下面的代码展示了不同数组长度下,List<int>与int[]遍历的性能差异。
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
using System.Collections.Generic;
[MemoryDiagnoser]
public class CollectionBenchmark
{
[Params(100, 1000, 10000)]
public int Size;
private int[] array;
private List<int> list;
[GlobalSetup]
public void Setup()
{
array = new int[Size];
list = new List<int>(Size);
for (int i = 0; i < Size; i++)
{
array[i] = i;
list.Add(i);
}
}
[Benchmark]
public int SumArray()
{
int sum = 0;
foreach (var item in array)
{
sum += item;
}
return sum;
}
[Benchmark]
public int SumList()
{
int sum = 0;
foreach (var item in list)
{
sum += item;
}
return sum;
}
}
public class Program
{
public static void Main(string[] args)
{
BenchmarkRunner.Run<CollectionBenchmark>();
}
}
这里的[GlobalSetup]特性标记的方法会在每次基准测试前执行一次,用于初始化数据。Size参数被[Params]标记后,BenchmarkDotNet会针对100、1000、10000三个规模分别运行SumArray和SumList,最终输出一张按Size分组的表格。这样就能观察到随着数据量增长,数组和List的遍历性能差距是否有变化。通常数组访问比List稍快,因为List内部虽然基于数组,但存在索引器调用和边界检查等额外开销。
[MemoryDiagnoser]特性会在报告中附加内存分配和GC数据。对于上面的例子,两个方法本身都没有分配新对象,Allocated应为0。如果某个基准方法内部会创建临时集合或字符串,内存诊断就能快速定位分配热点。内存分配优化对服务器应用尤其重要,减少Gen0回收频率可以显著降低延迟。
多运行时比较与常见陷阱
有时需要比较同一份代码在不同.NET运行时或不同依赖版本下的表现。BenchmarkDotNet支持在特性中声明运行时目标,例如[SimpleJob(RuntimeMoniker.Net80)]和[SimpleJob(RuntimeMoniker.Net60)]。运行时会自动选择本机已安装的对应版本,分别执行基准并汇总结果。这对于评估升级.NET版本是否带来性能提升非常有用,也可以比较自承载与框架依赖部署的差异。
不过做基准测试时,有几个常见陷阱会让数据失真。第一,忘记使用Release配置,Debug下的JIT不会进行优化,循环可能被插入额外的调试检查;第二,基准方法返回未被使用的值,JIT可能将整个计算优化掉,导致测量时间接近零,正确做法是返回计算结果或使用[MethodImpl(MethodImplOptions.NoInlining)];第三,没有预热,首次调用需要JIT编译,耗时会被计入,BenchmarkDotNet虽然会自动预热,但如果你把基准方法放在循环里手动测试,就会忽略这一点。
另一个容易被忽略的问题是共享状态。如果多个基准方法修改同一个静态字段,前一个方法的副作用会传递给后一个,导致结果无法复现。使用[IterationSetup]或[GlobalSetup]在每个迭代或整个基准运行前重置状态可以避免这种情况。另外,系统负载、电源模式、后台进程都会影响测量结果,建议关闭不必要的程序并保持稳定的运行环境。最后,基准测试不是一次性活动,当代码逻辑或依赖版本发生变化时,应重新运行基准并记录历史数据,观察性能趋势。
通过BenchmarkDotNet建立标准化的性能测量流程,不仅能给出可信的优化依据,还能防止被微基准中的偶然差异误导。把基准测试纳入代码评审和CI流程,让性能回归尽早暴露,是长期维护.NET应用性能的关键。
BenchmarkDotNet性能测试.NET基准测试修改时间:2026-08-24 03:13:47