SourceGenerator 生成 db to class 代码优化结果记录 二
- 2024-10-18 中国台湾
本文字数:4730 字
阅读完需:约 16 分钟
优化
在上一篇留下的 Dapper AOT 还有什么特别优化点的问题
在仔细阅读生成代码和源码之后,终于得到了答案
个人之前一直以为 Dapper AOT 只用了迭代器去实现,所以理应差不多实现代码却又极大差距,思维陷入了僵局,一度以为有什么黑魔法
结果 Dapper AOT 没有用迭代器去实现!!! 靠北啦,还以为迭代器有新姿势可以优化了
不再使用迭代器
List<BenchmarkTest.Dog> results = new();
try
{
while (reader.Read())
{
results.Add(ReadOne(reader, readOnlyTokens));
}
return results;
}
当然就只能要求 用户必须使用 AsList
方法,因为 ToList
会导致复制 list 的问题, 导致负优化,
像这样
connection.Query<Dog>("select * from dog").AsList();
// AsList 实现
public static List<T> AsList<T>(this IEnumerable<T>? source) => source switch
{
null => null!,
List<T> list => list,
_ => Enumerable.ToList(source),
};
使用 span
再没有了迭代器方法限制, span 就可以放飞自我,随意使用了
public static BenchmarkTest.Dog ReadOne(this IDataReader reader, ref ReadOnlySpan<int> ss)
{
var d = new BenchmarkTest.Dog();
for (int j = 0; j < ss.Length; j++)
{
使用 ArrayPool<int> 减少内存占用
public Span<int> GetTokens()
{
FieldCount = Reader!.FieldCount;
if (Tokens is null || Tokens.Length < FieldCount)
{
// no leased array, or existing lease is not big enough; rent a new array
if (Tokens is not null) ArrayPool<int>.Shared.Return(Tokens);
Tokens = ArrayPool<int>.Shared.Rent(FieldCount);
}
return MemoryMarshal.CreateSpan(ref MemoryMarshal.GetArrayDataReference(Tokens), FieldCount);
}
数据小时使用栈分配
var s = reader.FieldCount <= 64 ? MemoryMarshal.CreateSpan(ref MemoryMarshal.GetReference(stackalloc int[reader.FieldCount]), reader.FieldCount) : state.GetTokens();
提前生成部分 hashcode 进行比较
因为比较现在也并不耗时了, 所以 缓存也没有必要了, 也一并移除
public static void GenerateReadTokens(this IDataReader reader, Span<int> s)
{
for (int i = 0; i < reader.FieldCount; i++)
{
var name = reader.GetName(i);
var type = reader.GetFieldType(i);
switch (EntitiesGenerator.NormalizedHash(name))
{
case 742476188U:
s[i] = type == typeof(int) ? 1 : 2;
break;
case 2369371622U:
s[i] = type == typeof(string) ? 3 : 4;
break;
case 1352703673U:
s[i] = type == typeof(float) ? 5 : 6;
break;
default:
break;
}
}
}
性能测试说明
BenchmarkDotNet
这里特别说明一下
使用的 BenchmarkDotNet ,其本身已经考虑了 jit 优化等等方面, 有预热,超多次执行,
结果值也是按照统计学有考虑结果集分布情况处理,移除变差大的值(比如少数的孤立的极大极小值), 差异不大情况,一般显示平均值,有大差异时还会显示 中位值
感兴趣的童鞋可以去 https://github.com/dotnet/BenchmarkDotNet 了解
chole 有点棘手,为了方便 mock,所以 copy 了部分源码,只比较实体映射部分
测试数据
测试数据 正如之前说过, 采用 手动 mock 方式,避免 db 驱动 、db 执行、mock 库 等等 带来的执行差异影响
class
非常简单的类,当然不能代表所有情况,不过简单测试够用了
public class Dog
{
public int? Age { get; set; }
public string Name { get; set; }
public float? Weight { get; set; }
}
mock 数据
public class TestDbConnection : DbConnection
{
public int RowCount { get; set; }
public IDbCommand CreateCommand()
{
return new TestDbCommand() { RowCount = RowCount };
}
}
public class TestDbCommand : DbCommand
{
public int RowCount { get; set; }
public IDataParameterCollection Parameters { get; } = new TestDataParameterCollection();
public IDbDataParameter CreateParameter()
{
return new TestDataParameter();
}
protected override DbDataReader ExecuteDbDataReader(CommandBehavior behavior)
{
return new TestDbDataReader() { RowCount = RowCount };
}
}
public class TestDbDataReader : DbDataReader
{
public int RowCount { get; set; }
private int calls = 0;
public override object this[int ordinal]
{
get
{
switch (ordinal)
{
case 0:
return "XX";
case 1:
return 2;
case 2:
return 3.3f;
default:
return null;
}
}
}
public override int FieldCount => 3;
public override Type GetFieldType(int ordinal)
{
switch (ordinal)
{
case 0:
return typeof(string);
case 1:
return typeof(int);
case 2:
return typeof(float);
default:
return null;
}
}
public override float GetFloat(int ordinal)
{
switch (ordinal)
{
case 2:
return 3.3f;
default:
return 0;
}
}
public override int GetInt32(int ordinal)
{
switch (ordinal)
{
case 1:
return 2;
default:
return 0;
}
}
public override string GetName(int ordinal)
{
switch (ordinal)
{
case 0:
return "Name";
case 1:
return "Age";
case 2:
return "Weight";
default:
return null;
}
}
public override string GetString(int ordinal)
{
switch (ordinal)
{
case 0:
return "XX";
default:
return null;
}
}
public override object GetValue(int ordinal)
{
switch (ordinal)
{
case 0:
return "XX";
case 1:
return 2;
case 2:
return 3.3f;
default:
return null;
}
}
public override bool Read()
{
calls++;
return calls <= RowCount;
}
}
Benchmark 代码
[MemoryDiagnoser, Orderer(summaryOrderPolicy: SummaryOrderPolicy.FastestToSlowest), GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory), CategoriesColumn]
public class ObjectMappingTest
{
[Params(1, 1000, 10000, 100000, 1000000)]
public int RowCount { get; set; }
[Benchmark(Baseline = true)]
public void SetClass()
{
var connection = new TestDbConnection() { RowCount = RowCount };
var dogs = new List<Dog>();
try
{
connection.Open();
var cmd = connection.CreateCommand();
cmd.CommandText = "select ";
using (var reader = cmd.ExecuteReader(CommandBehavior.Default))
{
while (reader.Read())
{
var dog = new Dog();
dogs.Add(dog);
dog.Name = reader.GetString(0);
dog.Age = reader.GetInt32(1);
dog.Weight = reader.GetFloat(2);
}
}
}
finally
{
connection.Close();
}
}
[Benchmark]
public void DapperAOT()
{
var connection = new TestDbConnection() { RowCount = RowCount };
var dogs = connection.Query<Dog>("select * from dog").AsList();
}
[Benchmark]
public void SourceGenerator()
{
var connection = new TestDbConnection() { RowCount = RowCount };
List<Dog> dogs;
try
{
connection.Open();
var cmd = connection.CreateCommand();
cmd.CommandText = "select ";
using (var reader = cmd.ExecuteReader(CommandBehavior.Default))
{
dogs = reader.ReadTo<Dog>().AsList();
}
}
finally
{
connection.Close();
}
}
[Benchmark]
public void Chloe()
{
var connection = new TestDbConnection() { RowCount = RowCount };
try
{
connection.Open();
var cmd = connection.CreateCommand();
var dogs = new InternalSqlQuery<Dog>(cmd, "select").AsList();
}
finally
{
connection.Close();
}
}
}
完整代码可以参考 https://github.com/fs7744/SlowestEM
测试结果
BenchmarkDotNet v0.13.12, Windows 11 (10.0.22631.3880/23H2/2023Update/SunValley3)
13th Gen Intel Core i9-13900KF, 1 CPU, 32 logical and 24 physical cores
.NET SDK 9.0.100-preview.6.24328.19
[Host] : .NET 8.0.7 (8.0.724.31311), X64 RyuJIT AVX2
DefaultJob : .NET 8.0.7 (8.0.724.31311), X64 RyuJIT AVX2
SourceGenerator 基本等同 DapperAOT 了, 除了没有使用 Interceptor, 以及各种情况细节没有考虑之外, 两者性能一样
SourceGenerator 肯定现在性能优化最佳方式,毕竟可以生成代码文件,上手难度其实比 emit 之类小多了
版权声明: 本文为 InfoQ 作者【八苦-瞿昙】的原创文章。
原文链接:【http://xie.infoq.cn/article/ad1153331927c305e278fa2af】。文章转载请联系作者。
八苦-瞿昙
一个假和尚,不懂人情世故。 2018-11-23 加入
会点点技术,能写些代码,只爱静静。 g hub: https://github.com/fs7744 黑历史:https://www.cnblogs.com/fs7744
评论