BenchmarkDotNet 性能基准测试
大约 9 分钟约 2608 字
BenchmarkDotNet 性能基准测试
简介
性能优化不能靠猜测,需要数据驱动。BenchmarkDotNet 是 .NET 生态中最权威的性能基准测试框架,能自动处理预热、多次迭代、统计分析和结果对比。掌握 BenchmarkDotNet 可以精确测量代码执行时间、内存分配,为性能优化提供可靠的数据依据。
特点
基础基准测试
创建基准
/// <summary>
/// BenchmarkDotNet 基础用法
/// </summary>
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
[MemoryDiagnoser] // 启用内存诊断
public class StringConcatBenchmarks
{
private const int Iterations = 1000;
private readonly List<string> _items = Enumerable.Range(0, 100).Select(i => $"item{i}").ToList();
[Benchmark(Baseline = true)]
public string StringConcat()
{
string result = "";
for (int i = 0; i < _items.Count; i++)
result += _items[i];
return result;
}
[Benchmark]
public string StringBuilder()
{
var sb = new StringBuilder();
for (int i = 0; i < _items.Count; i++)
sb.Append(_items[i]);
return sb.ToString();
}
[Benchmark]
public string StringJoin()
{
return string.Join("", _items);
}
[Benchmark]
public string StringConcatLinq()
{
return string.Concat(_items);
}
}
// 运行
BenchmarkRunner.Run<StringConcatBenchmarks>();参数化测试
多场景对比
/// <summary>
/// 参数化基准测试
/// </summary>
[Config(typeof(Config))]
public class SerializationBenchmarks
{
private class Config : ManualConfig
{
public Config()
{
SummaryStyle = SummaryStyle.Default.WithMaxParameterColumnWidth(30);
}
}
private User _user = null!;
private List<User> _users = null!;
private string _json = null!;
[Params(100, 1000, 10000)]
public int Count { get; set; }
[GlobalSetup]
public void Setup()
{
_user = new User { Id = 1, Name = "张三", Email = "test@example.com" };
_users = Enumerable.Range(0, Count)
.Select(i => new User { Id = i, Name = $"用户{i}", Email = $"user{i}@test.com" })
.ToList();
_json = JsonSerializer.Serialize(_users);
}
[Benchmark]
public string SystemTextJson_Serialize()
{
return JsonSerializer.Serialize(_users);
}
[Benchmark]
public List<User> SystemTextJson_Deserialize()
{
return JsonSerializer.Deserialize<List<User>>(_json)!;
}
[Benchmark]
public string SystemTextJson_SourceGen()
{
return JsonSerializer.Serialize(_users, MyJsonContext.Default.ListUser);
}
}集合操作对比
LINQ vs 传统循环
/// <summary>
/// LINQ 与传统循环性能对比
/// </summary>
[MemoryDiagnoser]
[RankColumn]
public class CollectionBenchmarks
{
private List<int> _data = null!;
private int[] _array = null!;
[Params(1000, 100000)]
public int Size { get; set; }
[GlobalSetup]
public void Setup()
{
var random = new Random(42);
_data = Enumerable.Range(0, Size).Select(_ => random.Next(1000)).ToList();
_array = _data.ToArray();
}
// 求和
[Benchmark(Baseline = true)]
public int Sum_ForLoop()
{
int sum = 0;
for (int i = 0; i < _array.Length; i++)
sum += _array[i];
return sum;
}
[Benchmark]
public int Sum_LINQ()
{
return _data.Sum();
}
// 过滤
[Benchmark]
public List<int> Where_ForLoop()
{
var result = new List<int>();
for (int i = 0; i < _data.Count; i++)
{
if (_data[i] > 500)
result.Add(_data[i]);
}
return result;
}
[Benchmark]
public List<int> Where_LINQ()
{
return _data.Where(x => x > 500).ToList();
}
}字符串与 JSON
JSON 序列化对比
/// <summary>
/// JSON 序列化性能对比
/// </summary>
[MemoryDiagnoser]
[GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByParams)]
public class JsonBenchmarks
{
private Order _order = null!;
private string _orderJson = null!;
[GlobalSetup]
public void Setup()
{
_order = new Order
{
Id = "ORD-2024-001",
Customer = "张三",
Items = Enumerable.Range(1, 10)
.Select(i => new OrderItem { Product = $"商品{i}", Price = i * 99.9m, Qty = i })
.ToList(),
TotalAmount = 999.9m,
CreatedAt = DateTime.Now
};
_orderJson = JsonSerializer.Serialize(_order);
}
[Benchmark(Baseline = true)]
public string STJ_Serialize()
{
return JsonSerializer.Serialize(_order);
}
[Benchmark]
public Order STJ_Deserialize()
{
return JsonSerializer.Deserialize<Order>(_orderJson)!;
}
[Benchmark]
public string STJ_SourceGen()
{
return JsonSerializer.Serialize(_order, OrderContext.Default.Order);
}
}高级配置
自定义配置
/// <summary>
/// BenchmarkDotNet 高级配置
/// /// </summary>
var config = ManualConfig.Create(DefaultConfig.Instance)
.AddExporter(MarkdownExporter.GitHub)
.AddExporter(HtmlExporter.Default)
.AddDiagnoser(MemoryDiagnoser.Default)
.AddColumn(StatisticColumn.P95)
.AddColumn(StatisticColumn.StdDev)
.WithOptions(ConfigOptions.JoinSummary);
BenchmarkRunner.Run<MyBenchmarks>(config);
// 运行特定测试
BenchmarkRunner.Run<StringBenchmarks>(args: new[] { "--filter", "*StringBuilder*" });
// 控制台运行
// dotnet run -c Release -- --filter "*Benchmark*" --runtimes net8.0EF Core 查询性能基准测试
/// <summary>
/// EF Core 查询性能对比
/// </summary>
[MemoryDiagnoser]
public class EfCoreQueryBenchmarks
{
private AppDbContext _db = null!;
[GlobalSetup]
public void Setup()
{
var options = new DbContextOptionsBuilder<AppDbContext>()
.UseSqlServer("Server=localhost;Database=benchmark_db;Trusted_Connection=True")
.Options;
_db = new AppDbContext(options);
_db.Database.EnsureCreated();
}
[Benchmark(Baseline = true)]
public List<User> TrackingQuery()
{
return _db.Users.Take(1000).ToList();
}
[Benchmark]
public List<User> NoTrackingQuery()
{
return _db.Users.AsNoTracking().Take(1000).ToList();
}
[Benchmark]
public List<UserDto> ProjectionQuery()
{
return _db.Users
.Take(1000)
.Select(u => new UserDto { Id = u.Id, Name = u.Name })
.ToList();
}
[Benchmark]
public async Task<List<User>> AsyncQuery()
{
return await _db.Users.Take(1000).ToListAsync();
}
[Benchmark]
public async Task<List<UserDto>> CompiledQuery()
{
return await _compiledQuery(_db);
}
private static readonly Func<AppDbContext, Task<List<UserDto>>> _compiledQuery =
EF.CompileAsyncQuery((AppDbContext db) =>
db.Users.Take(1000).Select(u => new UserDto { Id = u.Id, Name = u.Name }).ToList());
[GlobalCleanup]
public void Cleanup()
{
_db.Dispose();
}
}
// 典型结果(1000 条记录):
// | Method | Mean | Allocated |
// |-----------------|----------|-----------|
// | TrackingQuery | 12.5 ms | 480 KB |
// | NoTrackingQuery | 6.2 ms | 320 KB |
// | ProjectionQuery | 3.1 ms | 120 KB |
// | AsyncQuery | 12.8 ms | 480 KB |
// | CompiledQuery | 5.8 ms | 120 KB |HTTP 服务性能基准测试
/// <summary>
/// ASP.NET Core HTTP 端点基准测试
/// </summary>
[MemoryDiagnoser]
public class HttpEndpointBenchmarks
{
private WebApplication _app = null!;
private HttpClient _client = null!;
[GlobalSetup]
public void Setup()
{
var builder = WebApplication.CreateBuilder();
builder.Services.AddControllers();
// 注册依赖
builder.Services.AddScoped<IUserService, UserService>();
builder.Services.AddDbContext<AppDbContext>(options =>
options.UseInMemoryDatabase("BenchmarkDb"));
_app = builder.Build();
_app.MapControllers();
_client = _app.CreateClient();
}
[Benchmark]
public async Task<HttpResponseMessage> GetUsers()
{
return await _client.GetAsync("/api/users?page=1&pageSize=10");
}
[Benchmark]
public async Task<HttpResponseMessage> CreateUser()
{
var content = new StringContent(
JsonSerializer.Serialize(new { name = "Test User", email = "test@example.com" }),
Encoding.UTF8,
"application/json");
return await _client.PostAsync("/api/users", content);
}
[GlobalCleanup]
public void Cleanup()
{
_app.Dispose();
_client.Dispose();
}
}依赖注入性能基准测试
/// <summary>
/// DI 容器性能对比
/// </summary>
[MemoryDiagnoser]
[RankColumn]
public class DiBenchmarks
{
private IServiceProvider _sp = null!;
private WebApplication _app = null!;
[GlobalSetup]
public void Setup()
{
var builder = WebApplication.CreateBuilder();
// 注册各种生命周期的服务
builder.Services.AddSingleton<ISingletonService, SingletonService>();
builder.Services.AddScoped<IScopedService, ScopedService>();
builder.Services.AddTransient<ITransientService, TransientService>();
_app = builder.Build();
_sp = _app.Services;
}
[Benchmark]
public object ResolveSingleton()
{
return _sp.GetRequiredService<ISingletonService>();
}
[Benchmark]
public object ResolveTransient()
{
return _sp.GetRequiredService<ITransientService>();
}
[Benchmark]
public object ResolveScoped()
{
using var scope = _sp.CreateScope();
return scope.ServiceProvider.GetRequiredService<IScopedService>();
}
[Benchmark]
public object ResolveGraph()
{
using var scope = _sp.CreateScope();
return scope.ServiceProvider.GetRequiredService<RootService>();
}
[GlobalCleanup]
public void Cleanup()
{
_app.Dispose();
}
}
// 典型结果:
// | Method | Mean | Allocated |
// |-----------------|-----------|-----------|
// | ResolveSingleton| 3.2 ns | 24 B |
// | ResolveTransient| 8.5 ns | 48 B |
// | ResolveScoped | 6.1 ns | 32 B |
// | ResolveGraph | 45.3 ns | 320 B |缓存性能基准测试
/// <summary>
/// 缓存策略性能对比
/// </summary>
[MemoryDiagnoser]
public class CacheBenchmarks
{
private IMemoryCache _cache = null!;
private readonly string[] _keys = Enumerable.Range(0, 1000)
.Select(i => $"key:{i}").ToArray();
[GlobalSetup]
public void Setup()
{
var services = new ServiceCollection();
services.AddMemoryCache();
var sp = services.BuildServiceProvider();
_cache = sp.GetRequiredService<IMemoryCache>();
// 预填充缓存
foreach (var key in _keys)
{
_cache.Set(key, $"value-{key}", TimeSpan.FromMinutes(30));
}
}
[Benchmark]
public string? CacheHit()
{
return _cache.Get<string>(_keys[0]);
}
[Benchmark]
public string? CacheMiss()
{
return _cache.Get<string>("nonexistent:key");
}
[Benchmark]
public string GetOrCreate()
{
return _cache.GetOrCreate("new:key", entry =>
{
entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10);
return "computed-value";
})!;
}
[Benchmark]
public void Remove()
{
_cache.Remove(_keys[0]);
}
}基准测试最佳实践
1. 必须在 Release 模式下运行
dotnet run -c Release
或在 csproj 中设置 <Optimize>true</Optimize>
2. 避免在 GlobalSetup 中做耗时操作
GlobalSetup 只在所有 Benchmark 之前执行一次
3. 使用 [IterationSetup] 和 [IterationCleanup]
每次迭代前后执行清理,避免状态干扰
4. 排除首次运行的影响
BenchmarkDotNet 自动处理预热(Warmup),无需手动排除
5. 注意 GC 的影响
使用 [MemoryDiagnoser] 查看 Gen0/Gen1/Gen2 分配
对于需要精确内存测试,使用 [GcForce] 强制 GC
6. 多环境对比
使用 [Config(typeof(Config))] 定义不同运行时版本
dotnet run -c Release -- --runtimes net6.0 net7.0 net8.0
7. 输出格式选择
- MarkdownExporter.GitHub: GitHub 友好
- HtmlExporter.Default: 浏览器查看
- CsvExporter.Default: Excel 分析
- JsonExporter.Full: 自动化处理
8. 避免在 Benchmark 中使用 Console.WriteLine
I/O 操作会严重影响结果
9. 避免死代码消除
确保返回值被使用,或使用 Consume() 方法
BenchmarkDotNet 会自动处理返回值
10. 长时间基准测试
使用 [LongRunJob] 或自定义 Job 配置
适合测试稳定性和长时间运行场景基准测试报告解读
典型输出格式:
| Method | Mean | Error | StdDev | Median | Min | Max | P95 | Gen0 | Allocated |
|-----------------|-----------|----------|----------|-----------|-----------|-----------|----------|--------|-----------|
| StringBuilder | 1.234 us | 0.012 us | 0.011 us | 1.230 us | 1.210 us | 1.260 us | 1.250 us | - | 1.45 KB |
| StringJoin | 0.567 us | 0.008 us | 0.007 us | 0.565 us | 0.555 us | 0.580 us | 0.575 us | - | 0.82 KB |
关键字段说明:
- Mean: 平均执行时间
- Error: 99.9% 置信区间的一半
- StdDev: 标准差(值越小越稳定)
- Median: 中位数
- P95: 95% 分位数
- Gen0/Gen1/Gen2: GC 各代回收次数
- Allocated: 每次调用分配的内存
判断标准:
- Mean 差异 > Error 才有意义
- StdDev / Mean < 5% 说明结果稳定
- Gen0 为 0 说明无短生命周期分配
- Allocated 为 0 说明无堆分配优点
缺点
总结
BenchmarkDotNet 是 .NET 性能测试的标配工具。使用方式:[Benchmark] 标注方法,[MemoryDiagnoser] 启用内存诊断,[Params] 参数化测试。必须在 Release 模式下运行。性能优化流程:先基准测试找瓶颈,再针对性优化,最后再次测试验证提升效果。
关键知识点
- 先分清这个主题位于请求链路、后台任务链路还是基础设施链路。
- 服务端主题通常不只关心功能正确,还关心稳定性、性能和可观测性。
- 任何框架能力都要结合配置、生命周期、异常传播和外部依赖一起看。
项目落地视角
- 画清请求进入、业务执行、外部调用、日志记录和错误返回的完整路径。
- 为关键链路补齐超时、重试、熔断、追踪和结构化日志。
- 把配置与敏感信息分离,并明确不同环境的差异来源。
常见误区
- 只会堆中间件或组件,不知道它们在链路中的执行顺序。
- 忽略生命周期和线程池、连接池等运行时资源约束。
- 没有监控和测试就对性能或可靠性下结论。
进阶路线
- 继续向运行时行为、可观测性、发布治理和微服务协同深入。
- 把主题和数据库、缓存、消息队列、认证授权联动起来理解。
- 沉淀团队级模板,包括统一异常处理、配置约定和基础设施封装。
适用场景
- 当你准备把《BenchmarkDotNet 性能基准测试》真正落到项目里时,最适合先在一个独立模块或最小样例里验证关键路径。
- 适合 API 服务、后台任务、实时通信、认证授权和微服务协作场景。
- 当需求开始涉及稳定性、性能、可观测性和发布流程时,这类主题会成为基础设施能力。
落地建议
- 先定义请求链路与失败路径,再决定中间件、过滤器、服务边界和依赖方式。
- 为关键链路补日志、指标、追踪、超时与重试策略。
- 环境配置与敏感信息分离,避免把生产参数写死在代码或镜像里。
排错清单
- 先确认问题发生在路由、模型绑定、中间件、业务层还是基础设施层。
- 检查 DI 生命周期、配置来源、序列化规则和认证上下文。
- 查看线程池、连接池、缓存命中率和外部依赖超时。
复盘问题
- 如果把《BenchmarkDotNet 性能基准测试》放进你的当前项目,最先要验证的输入、输出和失败路径分别是什么?
- 《BenchmarkDotNet 性能基准测试》最容易在什么规模、什么边界条件下暴露问题?你会用什么指标或日志去确认?
- 相比默认实现或替代方案,采用《BenchmarkDotNet 性能基准测试》最大的收益和代价分别是什么?
