不使用反射进行C#属性的运行时动态访问
-
- 公平的竞赛
- 公平的实现方式
- 换个思路,最直白的实现方式
摘要
单纯的反射带来灵活性的同时,也大大降低了应用程序的效率。本文将利用C#的各种技术,就如何实现动态的方法调用或属性访问做一些初步的研究。希望可以给同样需要提高反射性能的朋友一些帮助。
问题的抽象
反射可以用在很多的情景中,但是抽象来看就是用来访问编译时无法确定的成员。这成员可以是方法,也可以是属性。为了简化问题,我们把问题限定在属性的访问上。那么反射这个功能就可以抽象成下面这个接口。
/// <summary> /// Abstraction of the function of accessing member of a object at runtime. /// </summary> public interface IMemberAccessor { /// <summary> /// Get the member value of an object. /// </summary> /// <param name="instance">The object to get the member value from.</param> /// <param name="memberName">The member name, could be the name of a property of field. Must be public member.</param> /// <returns>The member value</returns> object GetValue(object instance, string memberName); /// <summary> /// Set the member value of an object. /// </summary> /// <param name="instance">The object to get the member value from.</param> /// <param name="memberName">The member name, could be the name of a property of field. Must be public member.</param> /// <param name="newValue">The new value of the property for the object instance.</param> void SetValue(object instance, string memberName, object newValue); }
下面我们就来探讨这个接口怎么实现才能达到最高效率。
没有优化的反射
使用反射是实现上面接口的最直观最简单的方式。代码如下:
public class ReflectionMemberAccessor : IMemberAccessor { public object GetValue(object instance, string memberName) { var propertyInfo = instance.GetType().GetProperty(memberName); if (propertyInfo != null) { return propertyInfo.GetValue(instance, null); } return null; } public void SetValue(object instance, string memberName, object newValue) { var propertyInfo = instance.GetType().GetProperty(memberName); if (propertyInfo != null) { propertyInfo.SetValue(instance, newValue, null); } } }
但是这种方式的效率让人望而却步。经过分析我们可以发现最慢的部分就是GetValue和SetValue这两个调用。
使用Delegate优化的反射
将PropertyInfo的XetValue代理起来是最简单的提高性能方法。而且也已经有很多人介绍了这种方式,
1. Fast Dynamic Property Field Accessors
2. 晚绑定场景下对象属性赋值和取值可以不需要PropertyInfo
如果仅仅是看到他们的测试结果,会以为晚绑定就可以让属性的动态访问的速度达到和直接取值一样的速度,会觉得这生活多么美好啊。但是如果你真的把这个技术用在个什么地方会发现根本不是这么回事儿。真实的生活会如老赵写的Fast Reflection Library中给出的测试结果一般。你会发现即使是晚绑定了或是Emit了,速度也是要比直接访问慢5-20倍。是老赵的实现方式有问题吗?当然不是。
公平的竞赛
这里明确一下我们要实现的功能是什么?我们要实现的功能是,用一组方法或是模式,动态地访问任何一个对象上的任何一个属性。而前面那些看些美好的测试,都只是在测试晚绑定后的委托调用的性能,而那测试用的晚绑定委托调用都是针对某个类的某个属性的。这不是明摆着欺负反射吗?虽然测试用的反射Invoke也是针对一个属性,但是反射的通用版本的性能也是差不多的,Invoke才是消耗的大头。这也是数据统计蒙人的最常见的手法,用自己最好的一部分和对方的最差的一部分去比较。但是我们真正关心的是整体。
用晚绑定这个特性去实现类似反射能实现的功能,是需要把每个类的每个属性都缓存起来,并且在使用的时候,根据当前对象的类型和所取的属性名查找对应的缓存好的晚绑定委托。这些功能在那些美好的测试结果中都完全没有体现出来。而老赵的Fast Reflection Libary实现了这些功能,所以测试结果看上去要差很多。但是这才是真实的数据。
公平的实现方式
为了文章的完整起见,Delegate反射的实现方式如下。(我这里为了简单起见,没有过多优化,如果你要用这个方法,还是有很大的优化空间的。)
方法有两种,一种是使用Delegate.CreateDelegate函数。一种是使用Expression Tree。
使用Delegate的核心代码分别如下所示:
internal class PropertyAccessor<T, P> : INamedMemberAccessor { private Func<T, P> GetValueDelegate; private Action<T, P> SetValueDelegate; public PropertyAccessor(Type type, string propertyName) { var propertyInfo = type.GetProperty(propertyName); if (propertyInfo != null) { GetValueDelegate = (Func<T, P>)Delegate.CreateDelegate(typeof(Func<T, P>), propertyInfo.GetGetMethod()); SetValueDelegate = (Action<T, P>)Delegate.CreateDelegate(typeof(Action<T, P>), propertyInfo.GetSetMethod()); } } public object GetValue(object instance) { return GetValueDelegate((T)instance); } public void SetValue(object instance, object newValue) { SetValueDelegate((T)instance, (P)newValue); } }
Delegate.CreateDelegate在使用上有一个要求,其生成的Delegate的签名必须与Method的声明一致。所以就有了上面使用泛型的方式。每个PropertyAccessor是针对特定属性的,要真正用起来还要用Dictionary做下Mapping。如下所示:
public class DelegatedReflectionMemberAccessor : IMemberAccessor { private static Dictionary<string, INamedMemberAccessor> accessorCache = new Dictionary<string, INamedMemberAccessor>(); public object GetValue(object instance, string memberName) { return FindAccessor(instance, memberName).GetValue(instance); } public void SetValue(object instance, string memberName, object newValue) { FindAccessor(instance, memberName).SetValue(instance, newValue); } private INamedMemberAccessor FindAccessor(object instance, string memberName) { var type = instance.GetType(); var key = type.FullName + memberName; INamedMemberAccessor accessor; accessorCache.TryGetValue(key, out accessor); if (accessor == null) { var propertyInfo = type.GetProperty(memberName); accessor = Activator.CreateInstance(typeof(PropertyAccessor<,>).MakeGenericType(type, propertyInfo.PropertyType), type, memberName) as INamedMemberAccessor; accessorCache.Add(key, accessor); } return accessor; } }
用ExpressionTree的生成委托的时候,也会遇到类型的问题,但是我们可以在ExpressionTree中对参数和返回值的类型进行处理,这样就不需要泛型的实现方式了。代码如下:
public class DelegatedExpressionMemberAccessor : IMemberAccessor { private Dictionary<string, Func<object, object>> getValueDelegates = new Dictionary<string, Func<object, object>>(); private Dictionary<string, Action<object, object>> setValueDelegates = new Dictionary<string, Action<object, object>>(); public object GetValue(object instance, string memberName) { var type = instance.GetType(); var key = type.FullName + memberName; Func<object, object> getValueDelegate; getValueDelegates.TryGetValue(key, out getValueDelegate); if (getValueDelegate == null) { var info = type.GetProperty(memberName); var target = Expression.Parameter(typeof(object), "target"); var getter = Expression.Lambda(typeof(Func<object, object>), Expression.Convert(Expression.Property(Expression.Convert(target, type), info), typeof(object)), target ); getValueDelegate = (Func<object, object>)getter.Compile(); getValueDelegates.Add(key, getValueDelegate); } return getValueDelegate(instance); } }
一个优化方式是,把这个类做成泛型类,那么key就可以只是memberName,这样就少去了type.FullName及一次字符串拼接操作。性能可以提高不少。但是这种委托式的访问就是性能上的极限了吗?如果是我就不用来写这篇文章了。
虽然山寨却更直接的方法
我们的目标是动态的访问一个对象的一个属性,一谈到动态总是会自然而然地想到反射。其实还有一个比较质朴的方式。就是让这个类自己去处理。还记得一开始定义的IMemberAccessor接口吗?如果我们所有的类的都实现了这个接口,那么就直接调用这个方法就是了。方式如下。
public class Man : IMemberAccessor { public string Name { get; set; } public int Age { get; set; } public DateTime Birthday { get; set; } public double Weight { get; set; } public double Height { get; set; } public decimal Salary { get; set; } public bool Married { get; set; } public object GetValue(object instance, string memberName) { var man = instance as Man; if (man != null) { switch (memberName) { case "Name": return man.Name; case "Age": return man.Age; case "Birthday": return man.Birthday; case "Weight": return man.Weight; case "Height": return man.Height; case "Salary": return man.Salary; case "Married": return man.Married; default: return null; } } else throw new InvalidProgramException(); } public void SetValue(object instance, string memberName, object newValue) { var man = instance as Man; if (man != null) { switch (memberName) { case "Name": man.Name = newValue as string; break; case "Age": man.Age = Convert.ToInt32(newValue); break; case "Birthday": man.Birthday = Convert.ToDateTime(newValue); break; case "Weight": man.Weight = Convert.ToDouble(newValue); break; case "Height": man.Height = Convert.ToDouble(newValue); break; case "Salary": man.Salary = Convert.ToDecimal(newValue); break; case "Married": man.Married = Convert.ToBoolean(newValue); break; } } else throw new InvalidProgramException(); } }
有人可能会担心用这种方式,属性多了之后性能会下降。如果你用Reflector之类的工具反编译一下生成的DLL,你就不会有这种顾虑了。C#对于 switch语句有相当力度的优化。简略地讲,当属性少时会将switch生成为一堆if else。对于字段类型为string,也会自动地转成dictionary + int。
经过测试这种方式比上面的缓存晚绑定的方式要快一倍。但是劣势也很明显,就是代码量太大了,而且不是一个好的设计,也不优雅。
用动态生成的工具函数让动态属性访问更快一些
上面的方法速度上其实是最有优势的,但是缺乏可操作性。但是如果我们能为每个类动态地生成两个Get/Set方法,那么这个方法就实际可用了。注意,这时的动态调用并不是反射调用了。生成的方式就是使用Expression Tree编译出函数。
又因为这个方式是每个类一个函数,不像之前的方式都是一个属性一个访问对象。我们就可以利用C#的另一个特性来避免Dictionary的使用——泛型类中的静态成员:如果GenericClass<T>中定义的静态成员staticMember,那么GenericClass<A>中的staticMember和GenericClass<B>中的staticMember是不共享的。虽然查找泛型类也需要额外的运行时工作,但是代价比Dictionary查询要低。
在这个方法中,既没有用到反射,也没有用到缓存Dictionary。能更好地保证与手工代码性能的一致度。
实现的代码如下,鉴于代码量,只列出了get方法的代码:
public class DynamicMethod<T> : IMemberAccessor { internal static Func<object, string, object> GetValueDelegate; public object GetValue(object instance, string memberName) { return GetValueDelegate(instance, memberName); } static DynamicMethod() { GetValueDelegate = GenerateGetValue(); } private static Func<object, string, object> GenerateGetValue() { var type = typeof(T); var instance = Expression.Parameter(typeof(object), "instance"); var memberName = Expression.Parameter(typeof(string), "memberName"); var nameHash = Expression.Variable(typeof(int), "nameHash"); var calHash = Expression.Assign(nameHash, Expression.Call(memberName, typeof(object).GetMethod("GetHashCode"))); var cases = new List<SwitchCase>(); foreach (var propertyInfo in type.GetProperties()) { var property = Expression.Property(Expression.Convert(instance, typeof(T)), propertyInfo.Name); var propertyHash = Expression.Constant(propertyInfo.Name.GetHashCode(), typeof(int)); cases.Add(Expression.SwitchCase(Expression.Convert(property, typeof(object)), propertyHash)); } var switchEx = Expression.Switch(nameHash, Expression.Constant(null), cases.ToArray()); var methodBody = Expression.Block(typeof(object), new[] { nameHash }, calHash, switchEx); return Expression.Lambda<Func<object, string, object>>(methodBody, instance, memberName).Compile(); } }
但是,好吧,问题来了。泛型类就意味着需要在写代码的时候,或者说编译时知道对象的类型。这样也不符合我们一开始定义的目标。当然解决方案也是有的,就是再把那个Dictionary缓存请回来。具体方式参考上面的给Delegate做缓存的代码。
还有一个问题就是,这种Switch代码的性能会随着Property数量的增长而呈现大致为线性的下降。会最终差于Delegate缓存方式的调用。但是好在这个临界点比较高,大致在40个到60个属性左右。
性能测试
我们先把所有的方式列一下。
- 直接的对象属性读写
- 单纯的反射
- 使用Delegate.CreateDelegate生成委托并缓存
- 使用Expression Tree生成属性访问委托并缓存
- 让对象自己实现IMemberAccessor接口,使用Switch Case。
- 为每个类生成IMemberAcessor接口所定义的函数。(非泛型方式调用)
- 为每个类生成IMemberAcessor接口所定义的函数。(泛型方式调用)
我们来看一下这6种实现对应的7种使用方式的性能。
Debug:执行1000万次
方法 | 第一次结果 | 第二次结果 |
直接调用 | 208ms | 227ms |
反射调用 | 21376ms | 21802ms |
Expression委托 | 4341ms | 4176ms |
CreateDelegate委托 | 4204ms | 4111ms |
对象自身Switch | 1653ms | 1338ms |
动态生成函数 | 2123ms | 2051ms |
(泛型)动态生成函数 | 1167ms | 1157ms |
Release:执行1000万次
方法 | 第一次结果 | 第二次结果 |
直接调用 | 73ms | 77ms |
反射调用 | 20693ms | 21229ms |
Expression委托 | 3852ms | 3853ms |
CreateDelegate委托 | 3704ms | 3748ms |
对象自身Switch | 1105ms | 1116ms |
动态生成函数 | 1678ms | 1722ms |
(泛型)动态生成函数 | 843ms | 862ms |
动态生成的函数比手写的Switch还要快的原因是手写的Switch要使用到Dictionary来将String类型的字段名映射到int值。而我们生成的Switch是直接使用属性的hashcode值进行的。所以会更快。完整的代码可以从这里下载到。