一個基於 SourceGenerator 生成 從 dbReader轉換為 class 資料的效能測試實驗

victor.x.qu發表於2024-07-30

好奇

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

相關文章