一个基于 SourceGenerator 生成 从 dbReader转换为 class 数据的性能测试实验
好奇
SourceGenerator 出现开始,好几年了,虽然一直好奇用SourceGenerator 生成代码 与 emit 等动态生成的代码会有多少差距,
但是一直特别懒,不想搞
其实 dapper aot 项目做了类似事情,不过功能特别积极,还引用了实验特性,所以还是想更为简单客观对比
本次乘着自己暂时性不懒了,做了一个基于 SourceGenerator 生成 从 dbReader转换为 class 数据的测试
no generate code when
- Generic Type (如果不用 emit 动态生成,还真无法处理未知类型 T)
- Anonymous Type (SourceGenerator 生成时机要早于匿名类生成,所以还没机会生成)
generate code
具体怎么做的就这里不写了,感兴趣参考 https://github.com/fs7744/SlowestEM
生成的代码带有一定 db结果动态类型处理,以此更接近实际使用
// <auto-generated/>
#pragma warning disable 8019 //disable 'unnecessary using directive' warning
using System;
using System.Data;
using System.Runtime.CompilerServices;
using System.Collections.Generic;
namespace SlowestEM.Generator
{
public static partial class Dog_Accessors
{
public static IEnumerable<BenchmarkTest.Dog> Read(IDataReader reader)
{
var s = new List<Action<BenchmarkTest.Dog, IDataReader>>(reader.FieldCount);
for (int i = 0; i < reader.FieldCount; i++)
{
var j = i;
switch (reader.GetName(j).ToLower())
{
case "age":
{
// int?
var needConvert = typeof(int) != reader.GetFieldType(i);
s.Add((d,r) => d.Age = DBExtensions.ReadToInt32Nullable(r,j,needConvert));
}
break;
case "name":
{
// string
var needConvert = typeof(string) != reader.GetFieldType(i);
s.Add((d,r) => d.Name = DBExtensions.ReadToString(r,j,needConvert));
}
break;
case "weight":
{
// float?
var needConvert = typeof(float) != reader.GetFieldType(i);
s.Add((d,r) => d.Weight = DBExtensions.ReadToFloatNullable(r,j,needConvert));
}
break;
default:
break;
}
}
while (reader.Read())
{
var d = new BenchmarkTest.Dog();
foreach (var item in s)
{
item?.Invoke(d,reader);
}
yield return d;
}
}
}
}
测试结果
mock db, 避免 db层实现性能和没有正确处理数据类型装箱拆箱问题
[Benchmark(Baseline = true), BenchmarkCategory("1")]
public void SetClassFirst()
{
Dog dog;
try
{
connection.Open();
var cmd = connection.CreateCommand();
cmd.CommandText = "select ";
using (var reader = cmd.ExecuteReader(CommandBehavior.Default))
{
if (reader.Read())
{
dog = new Dog();
dog.Name = reader.GetString(0);
dog.Age = reader.GetInt32(1);
dog.Weight = reader.GetFloat(2);
}
}
}
finally
{
connection.Close();
}
}
[Benchmark, BenchmarkCategory("1")]
public void DapperMappingFirst()
{
var dogs = connection.QueryFirst<Dog>("select ");
}
[Benchmark, BenchmarkCategory("1")]
public void SourceGeneratorMappingFirst()
{
Dog dog;
try
{
connection.Open();
var cmd = connection.CreateCommand();
cmd.CommandText = "select ";
using (var reader = cmd.ExecuteReader(CommandBehavior.Default))
{
dog = reader.ReadTo<Dog>().FirstOrDefault();
}
}
finally
{
connection.Close();
}
}
BenchmarkDotNet v0.13.12, Windows 10 (10.0.19045.4651/22H2/2022Update)
Intel Core i7-10700 CPU 2.90GHz, 1 CPU, 16 logical and 8 physical cores
.NET SDK 9.0.100-preview.5.24307.3
[Host] : .NET 8.0.6 (8.0.624.26715), X64 RyuJIT AVX2
DefaultJob : .NET 8.0.6 (8.0.624.26715), X64 RyuJIT AVX2
Method | Categories | Mean | Error | StdDev | Ratio | RatioSD | Gen0 | Gen1 | Allocated | Alloc Ratio |
---|---|---|---|---|---|---|---|---|---|---|
SetClassFirst | 1 实体 | 18.38 ns | 0.378 ns | 0.316 ns | 1.00 | 0.00 | 0.0181 | - | 152 B | 1.00 |
SourceGeneratorMappingFirst | 1 实体 | 183.31 ns | 3.525 ns | 3.462 ns | 9.98 | 0.14 | 0.0899 | - | 752 B | 4.95 |
DapperMappingFirst | 1 实体 | 1,336.69 ns | 5.777 ns | 5.121 ns | 72.77 | 1.30 | 0.0343 | - | 288 B | 1.89 |
SetClass | 1000 实体 | 7,700.08 ns | 87.311 ns | 68.167 ns | 1.00 | 0.00 | 6.7749 | 1.1139 | 56712 B | 1.00 |
SourceGeneratorMapping | 1000 实体 | 23,428.85 ns | 262.698 ns | 232.875 ns | 3.04 | 0.03 | 6.8359 | 1.1292 | 57312 B | 1.01 |
DapperMapping | 1000 实体 | 48,880.92 ns | 682.693 ns | 533.002 ns | 6.35 | 0.06 | 13.4888 | 2.1362 | 113048 B | 1.99 |