写点什么

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 之类小多了

发布于: 刚刚阅读数: 4
用户头像

八苦-瞿昙

关注

一个假和尚,不懂人情世故。 2018-11-23 加入

会点点技术,能写些代码,只爱静静。 g hub: https://github.com/fs7744 黑历史:https://www.cnblogs.com/fs7744

评论

发布
暂无评论
SourceGenerator 生成db to class代码优化结果记录 二_dotnet_八苦-瞿昙_InfoQ写作社区