SourceGenerator 生成db to class代码优化结果记录 二

优化

在上一篇留下的 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 减少内存占用

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


MethodRowCountMeanErrorStdDevRatioRatioSDGen0Gen1Gen2AllocatedAlloc Ratio
DapperAOT1294.1 ns5.79 ns7.73 ns0.630.020.02340.0229-440 B1.00
Dapper1321.7 ns6.40 ns5.99 ns0.700.020.04050.0401-768 B1.75
SourceGenerator1408.7 ns6.38 ns5.33 ns0.890.010.02340.0229-440 B1.00
SetClass1460.6 ns4.82 ns4.51 ns1.000.000.02340.0229-440 B1.00
Chloe1498.9 ns8.99 ns12.31 ns1.090.030.04530.0448-856 B1.95
SetClass10004,751.0 ns84.12 ns86.38 ns1.000.003.02121.2894-56912 B1.00
SourceGenerator100011,402.9 ns220.27 ns244.83 ns2.390.043.02121.2817-56912 B1.00
DapperAOT100011,421.3 ns121.00 ns113.18 ns2.410.053.02120.6409-56912 B1.00
Dapper100029,601.8 ns447.50 ns396.69 ns6.250.155.55421.0986-105192 B1.85
Chloe100066,872.0 ns150.27 ns133.21 ns14.120.272.92970.9766-57328 B1.01
SetClass10000106,271.3 ns2,111.19 ns3,468.75 ns1.000.0041.626041.626041.6260662782 B1.00
DapperAOT10000172,867.7 ns2,079.77 ns1,945.42 ns1.650.0541.503941.503941.5039662782 B1.00
SourceGenerator10000181,916.1 ns1,653.15 ns1,465.47 ns1.740.0541.503941.503941.5039662782 B1.00
Dapper10000705,883.0 ns8,517.90 ns7,550.89 ns6.740.1982.031381.054741.01561143062 B1.72
Chloe10000746,825.0 ns3,067.25 ns2,869.11 ns7.150.2141.015641.015641.0156663198 B1.00
SetClass1000001,191,303.2 ns20,831.95 ns19,486.22 ns1.000.00498.0469498.0469498.04696098016 B1.00
DapperAOT1000001,794,197.8 ns17,937.20 ns16,778.47 ns1.510.03498.0469498.0469498.04696098016 B1.00
SourceGenerator1000001,973,894.9 ns26,063.73 ns24,380.03 ns1.660.03496.0938496.0938496.09386098016 B1.00
Dapper1000004,357,237.9 ns85,065.76 ns83,545.95 ns3.660.09492.1875492.1875492.187510898296 B1.79
Chloe1000007,524,264.2 ns91,289.38 ns85,392.15 ns6.320.14492.1875492.1875492.18756098432 B1.00
SetClass100000049,990,270.7 ns987,172.66 ns1,829,787.82 ns1.000.003300.00003300.00001400.000056778489 B1.00
DapperAOT100000056,473,264.7 ns995,473.43 ns1,427,678.25 ns1.130.053555.55563555.55561777.777856779066 B1.00
SourceGenerator100000058,368,836.3 ns1,153,542.14 ns2,080,074.43 ns1.170.063555.55563555.55561777.777856779066 B1.00
Chloe1000000110,416,752.0 ns1,562,298.26 ns1,461,374.77 ns2.190.103400.00003400.00001600.000056781312 B1.00
Dapper1000000138,433,886.4 ns2,765,190.70 ns4,385,885.48 ns2.770.146250.00006250.00002000.0000104779052 B1.85

SourceGenerator 基本等同 DapperAOT 了, 除了没有使用 Interceptor, 以及各种情况细节没有考虑之外, 两者性能一样

SourceGenerator 肯定现在性能优化最佳方式,毕竟可以生成代码文件,上手难度其实比 emit 之类小多了

原创作者: fs7744 转载于: https://www.cnblogs.com/fs7744/p/18340422
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值