泛型T未知时调用缓存委托的技术问题咨询
关于ORM中反射性能优化与委托缓存的解决方案
我之前在开发轻量ORM的时候也碰到过一模一样的问题——反射直接调用GetValue/SetValue的性能在数据量上来之后简直没法看,尤其是还要处理嵌套对象的时候,每行数据都要反复实例化、遍历属性,资源消耗大到离谱。用委托缓存确实是解决这个问题的核心思路,我来分享下我踩过的坑和实际可行的优化方案:
1. 基础Getter/Setter委托缓存:把反射开销一次性抹平
反射的最大瓶颈在于每次调用GetValue/SetValue时的动态类型解析,所以我们可以提前为每个属性生成强类型的委托,并存到线程安全的缓存字典里,后续直接调用委托即可,完全避免重复反射的开销。
举个生成Setter委托的示例代码:
public static Action<T, object> CreateSetter<T>(PropertyInfo property) { // 获取属性的Set方法(允许访问非公共方法) var setMethod = property.GetSetMethod(true); if (setMethod == null) throw new InvalidOperationException($"Property {property.Name} has no setter."); // 构建表达式树 var objParam = Expression.Parameter(typeof(T), "obj"); var valueParam = Expression.Parameter(typeof(object), "value"); // 把object类型的参数转换为属性实际类型 var convertedValue = Expression.Convert(valueParam, property.PropertyType); var callSet = Expression.Call(objParam, setMethod, convertedValue); // 编译为委托并返回 return Expression.Lambda<Action<T, object>>(callSet, objParam, valueParam).Compile(); }
Getter委托的生成逻辑类似:
public static Func<T, object> CreateGetter<T>(PropertyInfo property) { var getMethod = property.GetGetMethod(true); if (getMethod == null) throw new InvalidOperationException($"Property {property.Name} has no getter."); var objParam = Expression.Parameter(typeof(T), "obj"); var callGet = Expression.Call(objParam, getMethod); // 把属性值转换为object类型返回 var convertedResult = Expression.Convert(callGet, typeof(object)); return Expression.Lambda<Func<T, object>>(convertedResult, objParam).Compile(); }
缓存的时候一定要用线程安全的字典,比如ConcurrentDictionary,避免多线程环境下的缓存异常:
private static readonly ConcurrentDictionary<PropertyInfo, Delegate> _propertyAccessorCache = new ConcurrentDictionary<PropertyInfo, Delegate>(); // 封装一个获取Setter的方法,自动缓存 public static Action<T, object> GetSetter<T>(PropertyInfo property) { return (Action<T, object>)_propertyAccessorCache.GetOrAdd(property, prop => CreateSetter<T>(prop)); }
2. 嵌套对象的处理:递归缓存+实例化委托优化
嵌套对象的痛点在于需要反复实例化子对象并填充属性,这里可以做两个关键优化:
- 缓存类型实例化委托:用
Expression.New生成创建对象的委托,替代性能较差的Activator.CreateInstance,比如:
这样每次创建对象时,直接调用缓存的委托即可,开销极低。private static readonly ConcurrentDictionary<Type, Func<object>> _instanceFactoryCache = new ConcurrentDictionary<Type, Func<object>>(); public static Func<object> GetInstanceFactory(Type type) { return _instanceFactoryCache.GetOrAdd(type, t => Expression.Lambda<Func<object>>(Expression.New(t)).Compile() ); } - 递归处理嵌套属性:当遍历到属性类型为自定义引用类型(排除string、值类型等)时,先调用该类型的实例化委托创建对象,再递归填充其属性,同时把子对象的Getter/Setter也加入缓存,避免重复生成。
3. 你可能碰到的坑与排查要点
- 线程安全问题:所有缓存字典必须用
ConcurrentDictionary,或者在写入时加锁,否则多线程场景下会出现缓存丢失、委托重复编译甚至异常。 - 非公共成员访问:如果对象有私有构造函数或私有属性,生成委托时一定要确保
GetGetMethod(true)/GetSetMethod(true)的参数为true,允许访问非公共成员。 - 装箱拆箱开销:上面的示例用
object作为委托的参数/返回值,会有轻微的装箱拆箱开销。如果追求极致性能,可以生成强类型委托(比如Action<T, TProp>),但缓存Key需要包含属性类型,实现起来稍复杂,不过性能提升明显。 - 继承属性遍历:如果对象有继承关系,遍历属性时要使用
GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.FlattenHierarchy),确保能获取到父类的公共属性。
4. 更进一步的极致优化
如果你的ORM需要处理超大量数据,还可以考虑这些进阶方案:
- 直接生成IL代码:用
ILGenerator手动编写IL来生成访问器委托,比Expression树的性能略高,但代码可读性较差,适合对性能要求极高的场景。 - 批量属性访问器缓存:为每个类型缓存一个完整的属性访问器集合(比如
Dictionary<string, (Func<object, object> Getter, Action<object, object> Setter)>),这样根据属性名直接获取委托,减少字典查找次数。 - 对象池复用:对于频繁创建的对象,可以用对象池来复用实例,避免频繁GC的开销。
内容的提问来源于stack exchange,提问作者KalaNag
相关产品推荐
相关产品推荐

