DBShadow.net之性能优化的坎坷路

一、mysql参数的成本

  • 使用BenchmarkDotNet测试

1. 测试代码如下

  • CreateParameter直接构造参数
  • Clone预先构造参数名和类型,复制后只设置参数值
private static readonly MySqlCommand _command = new();
private static MySqlParameter _idParameter;

[Benchmark(Baseline = true)]
public DbParameter CreateParameter()
{
    var id = _command.CreateParameter();
    id.ParameterName = "Id";
    id.DbType = System.Data.DbType.Int64;
    id.Value = 1L;
    return id;
}
[Benchmark]
public DbParameter Clone()
{
    var id = _idParameter.Clone();
    id.Value = 1L;
    return id;
}

[GlobalSetup]
public void Setup()
{
    _idParameter = _command.CreateParameter();
    _idParameter.ParameterName = "Id";
    _idParameter.DbType = System.Data.DbType.Int64;
}

2. 测试结果如下

  • 通过复制方式节省了80%的时间
  • 感觉有搞头,所以希望把复制参数的功能加入到DBShadow.net中用来提高性能
Method Mean Error StdDev Ratio Gen0 Allocated Alloc Ratio
CreateParameter 58.60 ns 0.185 ns 0.213 ns 1.00 0.0064 112 B 1.00
Clone 11.01 ns 0.387 ns 0.431 ns 0.19 0.0064 112 B 1.00

二、Clone重写参数预编译

1. 生成的代码如下

  • 预编译反射command和参数类型,以便生成更原生更快的代码
  • 其中cached是预先构造好的参数缓存作为常量
  • cached含有参数名和类型信息
  • Clone后只需要设置参数值即可
(DbCommand command, Todo param) =>
{
    MySqlParameterCollection parameters = ((MySqlCommand)command).Parameters;
    MySqlParameter t = cached.Clone();
    t.Value = param.Id;
    // MySqlParameter Add(MySqlParameter parameter)
    return parameters.Add(t);
}

2. 选择更合适的原生方法

  • MySqlParameterCollection有两个Add方法
  • 很明显重载Add(MySqlParameter parameter)更合适
  • 调用重载Add(object value)需要做多次类型转换
  • 既然生成的代码可以控制,就需要选用更合适的重载
public MySqlParameter Add(MySqlParameter parameter);
public override int Add(object value);

3. 类型嗅探

  • 预编译是ShadowBuilder负责,基于ADO.net(DbCommand)
  • 为了生成更原生的代码,需要知道具体的Command类型
  • 所以ShadowBuilder需要更多实际的类型信息
  • 为此本来ShadowExecutor才需要的数据源信息,现在ShadowBuilder也需要

3.1 ShadowBuilder以前的代码

class ShadowBuilder(ISqlEngine engine, IMapperOptions options);

3.2 ShadowBuilder现在的代码

  • 其中CommandBuilder可以通过DbDataSourcet推导出来
  • 也就是实际只增加了DbDataSource
class ShadowBuilder(IMapperOptions options, ISqlEngine engine, DbDataSource dataSource, CommandBuilder commandBuilder);
var command = dataSource.CreateConnection().CreateCommand();
var parameterType = command.CreateParameter().GetType();
CommandBuilder commandBuilder = CommandBuilder.Create(command, parameterType);

3.3 嗅探的过程

  • 通过DbDataSource推导出CommandBuilder
  • 其中CreateConnection、CreateCommand和CreateParameter等方法并不实际执行数据库IO操作,只是用来嗅探类型信息
var command = dataSource.CreateConnection().CreateCommand();
var commandType = command.GetType();
var parametersProperty = commandType.GetProperty("Parameters", BindingFlags.Instance | BindingFlags.DeclaredOnly);
var parametersType = parametersProperty.PropertyType;
var parameterType = command.CreateParameter().GetType();
var addParameterMethod = parametersType.GetMethod("Add", [parameterType]);

4. ShadowExecutor变化比较小

  • 只是把类ShadowBuilder改为IShadowBuilder接口
  • 实际还是ShadowBuilder变化

4.1 ShadowExecutor以前的代码

class ShadowExecutor(ShadowBuilder builder, SqlSource source);

4.2 ShadowExecutor现在的代码

class ShadowExecutor(IShadowBuilder builder, SqlSource source);

5. ShadowBuilder和ShadowCachedBuilder的关系

5.1 ShadowCachedBuilder以前是ShadowBuilder的子类

class ShadowCachedBuilder : ShadowBuilder;

5.2 ShadowCachedBuilder现在是ShadowBuilder的包装类

  • 通过接口IShadowBuilder来实现
  • 通过original成员调用原有的ShadowBuilder功能
  • 这样避免ShadowBuilder的复杂度增加影响到ShadowCachedBuilder
  • ShadowCachedBuilder只负责缓存编译好的对象
class ShadowCachedBuilder(ShadowBuilder original)
    : IShadowBuilder;

三、复杂的现实世界

1. SqliteParameter不支持复制

  • SqliteParameter没有实现ICloneable接口
  • SqliteParameter也没有Clone方法

2. SqlParameter可以复制,但性能不佳

  • SqlParameter(Mssql)实现了ICloneable接口

2.1 测试代码如下

private readonly SqlCommand _command = new(); 
private ICloneable _idParameter;

[Benchmark(Baseline = true)]
public DbParameter CreateParameter()
{
    var id = _command.CreateParameter();
    id.ParameterName = "Id";
    id.DbType = System.Data.DbType.Int64;
    id.Value = 1L;
    return id;
}
[Benchmark]
public DbParameter Clone()
{
    var id = (SqlParameter)_idParameter.Clone();
    id.Value = 1L;
    return id;
}
[GlobalSetup]
public void Setup()
{
    var idParameter = _command.CreateParameter();
    idParameter.ParameterName = "Id";
    idParameter.DbType = System.Data.DbType.Int64;
    _idParameter = idParameter;
}

2.2 测试结果如下

  • Clone方式比直接CreateParameter方式还慢
  • 这就是为什么CommandBuilder可以推导出来还要作为ShadowBuilder参数的原因
  • 现在只能把选择权交给用户,让用户决定是否启用Clone方式
Method Mean Error StdDev Median Ratio RatioSD Gen0 Allocated Alloc Ratio
CreateParameter 15.38 ns 0.942 ns 0.967 ns 14.53 ns 1.00 0.09 0.0106 184 B 1.00
Clone 24.84 ns 0.169 ns 0.194 ns 24.81 ns 1.62 0.10 0.0106 184 B 1.00

四、ParameterBuilder

  • ParameterBuilder负责反射参数实际类型和方法,用于生成更高效的代码
  • ParameterBuilder是抽象类,有3个具体实现

1. BuildByNamedConstructor

  • 通过含参数名构造函数创建参数

2. BuildByDefaultConstructor

  • 通过默认构造函数创建参数,然后设置参数名

3. BuildByMethod

  • 通过command的CreateParameter方法创建参数,然后设置参数名

4. ParameterBuilder的成员

  • New方法用于创建参数实例
  • SetParameterName方法用于设置参数的ParameterName属性
  • SetDbType方法用于设置参数的DbType属性
  • SetValue方法用于设置参数的Value属性
  • GetParameterName方法用于生成集合参数的子参数名

5. 集合参数

  • 大部分数据库并不支持集合参数
  • 因此需要生成多个单独的子参数来实际执行
  • eg: IN @Ids 需要转化为 IN (@Ids0, @Ids1, @Ids2),Ids为集合参数
  • 集合参数很特殊,需要实际执行的时候才确定实际子参数的个数
  • 通过预编译可以生成遍历实参集合生成子参数的代码

五、IParameterFactory

  • IParameterFactory就是参数处理的抽象
  • ParameterFactory是默认实现
  • CloneParameterFactory是Clone方式实现
  • 而IParameterFactory作为CommandBuilder的成员,处理参数部分的逻辑

1. ParameterFactory

  • 默认的参数处理实现
  • ParameterFactory包含ParameterBuilder,用于生成更高效的参数处理代码
  • CloneParameterFactory也需要调用ParameterFactory的功能

1.1 数据库类型处理

  • 可以通过重写CheckDbTypeCore方法来处理特殊的数据库类型映射需求
/// <summary>
/// 处理数据库类型(预留扩展处理特殊需求)
/// </summary>
/// <param name="valueType"></param>
protected virtual DbType CheckDbTypeCore(Type valueType)
    => CheckDbType(valueType);
/// <summary>
/// 默认数据库类型
/// </summary>
/// <param name="valueType"></param>
/// <returns></returns>
public static DbType CheckDbType(Type valueType)
{
    if (valueType.IsArray)
        return DbType.Binary;
    var typeCode = Type.GetTypeCode(valueType);
    return typeCode switch
    {
        TypeCode.Byte => DbType.Byte,
        TypeCode.SByte => DbType.SByte,
        TypeCode.Int16 => DbType.Int16,
        TypeCode.UInt16 => DbType.UInt16,
        TypeCode.Int32 => DbType.Int32,
        TypeCode.UInt32 => DbType.UInt32,
        TypeCode.Int64 => DbType.Int64,
        TypeCode.UInt64 => DbType.UInt64,
        TypeCode.Single => DbType.Single,
        TypeCode.Double => DbType.Double,
        TypeCode.Decimal => DbType.Decimal,
        TypeCode.Boolean => DbType.Boolean,
        TypeCode.String => DbType.String,
        TypeCode.Char => DbType.StringFixedLength,
        TypeCode.DateTime => DbType.DateTime,
        _ => DbType.Object,
    };
}

1.2 CreateParameter方法

  • 实际CreateParameter专用于CloneParameterFactory
  • 用于创建参数原型,以便后续Clone使用
/// <summary>
/// 构造参数
/// </summary>
/// <param name="valueType"></param>
/// <returns></returns>
DbParameter CreateParameter(Type valueType);
/// <summary>
/// 构造参数
/// </summary>
/// <param name="name"></param>
/// <param name="valueType"></param>
/// <returns></returns>
DbParameter CreateParameter(string name, Type valueType);

1.3 实现接口IParameterBuilder

  • 以下Create方法实际用于构造参数表达式
  • 其中_parameterBuilder就是ParameterBuilder实例,前面有介绍
public Expression Create(IEmitBuilder builder, string name, Expression value)
    => Create(builder, Expression.Constant(name), value);
/// <summary>
/// 构造参数
/// </summary>
/// <param name="builder"></param>
/// <param name="parameterName"></param>
/// <param name="value"></param>
/// <returns></returns>
public Expression Create(IEmitBuilder builder, Expression parameterName, Expression value)
{
    // var parameter = command.CreateParameter();
    // parameter.ParameterName = parameterName;
    var parameter = _parameterBuilder.New(builder, parameterName);
    // parameter.DbType = dbType;
    _parameterBuilder.SetDbType(builder, parameter, CheckDbTypeCore(value.Type));
    // parameter.Value = value;
    _parameterBuilder.SetValue(builder, parameter, value);
    return parameter;
}

1.4 实现接口ICollectParameterBuilder

  • ICollectParameterBuilder用于处理集合参数的子参数
public Expression CreateIndex(IEmitBuilder builder, Expression prefix, Expression index, Expression value)
    => Create(builder, ParameterBuilder.GetParameterName(prefix, index), value);

2. CloneParameterFactory

  • CloneParameterFactory由CloneParameterBuilder和CloneCollectParameterBuilder组成
  • CloneParameterBuilder通过Clone方式创建参数
  • CloneCollectParameterBuilder通过Clone方式创建集合参数

3. CloneParameterBuilder

3.1 CloneParameterBuilder包含ParameterBuilder、ParameterFactory和IEmitConverter成员

  • ParameterBuilder用于处理参数值
  • ParameterFactory用于生成参数原型,以便Clone使用
  • IEmitConverter用于复制参数
class CloneParameterBuilder(ParameterBuilder original, ParameterFactory factory, IEmitConverter cloneConverter);

3.2 GetProtoType方法

  • 用于生成参数原型并缓存
  • 调用ParameterFactory的CreateParameter方法生成参数原型
  • 按参数名和类型缓存
    /// <summary>
    /// 获取原型缓存
    /// </summary>
    /// <param name="name"></param>
    /// <param name="valueType"></param>
    /// <returns></returns>
    public DbParameter GetProtoType(string name, Type valueType)
    {
        var key = new NameTypedCacheKey(name, valueType);
        if (_protoTypes.TryGetValue(key, out var cached))
            return cached;
#if NET9_0_OR_GREATER
        lock (_lock)
#else
        lock (_protoTypes)
#endif
        {
            if (_protoTypes.TryGetValue(key, out cached))
                return cached;
            return _protoTypes[key] = _factory.CreateParameter(name, valueType);
        }
    }

3.3 Create方法

  • Create用于构造参数表达式
  • 先调用GetProtoType方法获取参数原型
  • 该过程发生在预编译阶段,不会影响运行时性能
  • 通过缓存避免不同方法相同参数原型的重复创建
  • 通过cloneConverter生成Clone调用表达式
  • 最后调用ParameterBuilder设置参数值

4. CloneCollectParameterBuilder

  • CloneCollectParameterBuilder用于处理集合参数

3.1 CloneCollectParameterBuilder也是包含ParameterBuilder、ParameterFactory和IEmitConverter成员

  • ParameterBuilder用于处理参数值
  • ParameterFactory用于生成参数原型,以便Clone使用
  • IEmitConverter用于复制参数
class CloneCollectParameterBuilder(ParameterBuilder original, ParameterFactory factory, IEmitConverter cloneConverter);

3.2 GetProtoType方法

  • 用于生成集合参数原型并缓存
  • 调用ParameterFactory的CreateParameter方法生成参数原型
  • 按类型缓存
  • CollectParameterPrototype(集合参数原型)用于实际处理集合参数的子参数
    /// <summary>
    /// 获取原型缓存
    /// </summary>
    /// <param name="valueType"></param>
    /// <returns></returns>
    public CollectParameterPrototype GetProtoType(Type valueType)
    {
        if (_protoTypes.TryGetValue(valueType, out var protoType))
            return protoType;
#if NET9_0_OR_GREATER
        lock (_lock)
#else
        lock (_protoTypes)
#endif
        {
            if (_protoTypes.TryGetValue(valueType, out protoType))
                return protoType;
            var parameter = _factory.CreateParameter(valueType);
            return _protoTypes[valueType] = new(_original, parameter, _cloneConverter);
        }
    }

4. CollectParameterPrototype

  • 集合参数原型

4.1 包含ParameterBuilder、IEmitConverter和原型缓存

class CollectParameterPrototype(ParameterBuilder original, ConstantExpression cached, IEmitConverter cloneConverter);

4.2 CreateIndex方法

  • CreateIndex用于构造集合参数的子参数
  • 通过cloneConverter生成Clone调用表达式
  • 最后调用ParameterBuilder设置参数名和参数值

六、ToExecutor

  • 由于ShadowBuilder包含数据源可以很方便转化为ShadowExecutor
  • 增加ToExecutor方法简化操作
  • ToExecutor是IShadowBuilder的方法,所以ShadowCachedBuilder也支持
/// <summary>
/// 转化为执行器
/// </summary>
/// <param name="commandTimeout"></param>
/// <returns></returns>
ShadowExecutor ToExecutor(int? commandTimeout = null);

七、总结

  • 通过嗅探实际类型生成更高效的参数处理代码
  • 不同数据库差异为此带来了复杂性
  • 部分数据库可以通过Clone方式创建参数提高性能

1. 一般使用示例

  • 一般使用CreateCache方法创建ShadowBuilder
  • 如果是一次性使用无需缓存可以使用Create方法代替
var engine = new MySqlEngine();
var dataSource = new MySqlDataSource(ConnectionString);
var builder = ShadowBuilder.CreateCache(Mapper.Default, engine, dataSource);
var select = table.ToQuery()
    .And(table.Id.Equal())
    .ToSelect()
    .SelectSelfColumns();
var compiled = select.BuildQuery<Todo, Todo>(builder);

1.1 参数处理生成如下代码

(DbCommand command, Todo param) =>
{
    var parameters = ((MySqlCommand)command).Parameters;
    var t = new MySqlParameter();
    t.ParameterName = "Id";
    t.DbType = DbType.Int64;
    t.Value = param.Id;

    return parameters.Add(t);
}

2. 启用Clone方式示例

  • 启用Clone要复杂一点
  • 需要通过CloneParameter生成一个ParameterFactory
  • 然后传入ShadowBuilder的CreateCache方法中
var engine = new MySqlEngine();
var dataSource = new MySqlDataSource(ConnectionString);
var parameterFactory = ParameterFactory.CloneParameter(new MySqlCommand())
var builder = ShadowBuilder.CreateCache(Mapper.Default, engine, dataSource, parameterFactory);
var select = table.ToQuery()
    .And(table.Id.Equal())
    .ToSelect()
    .SelectSelfColumns();
var compiled = select.BuildQuery<Todo, Todo>(builder);

2.1 参数处理生成如下代码

  • cached是预先构造好作为常量的参数缓存
  • 很明显如果Clone比new更快的话,这个代码会更高效
  • Mysql下此方法耗时为原来的20%(也就是性能提高4倍)
(DbCommand command, Todo param) =>
{
    var parameters = ((MySqlCommand)command).Parameters;
    var t = cached.Clone();
    t.Value = param.Id;

    return parameters.Add(t);
}

另外源码托管地址: https://github.com/donetsoftwork/DBShadow.net ,欢迎大家直接查看源码。
gitee同步更新:https://gitee.com/donetsoftwork/DBShadow.net

如果大家喜欢请动动您发财的小手手帮忙点一下Star,谢谢!!!

posted on 2026-01-23 14:08  xiangji  阅读(101)  评论(2)    收藏  举报

导航