-
- 公平的競賽
- 公平的實現方式
- 換個思路,最直白的實現方式
摘要
單純的反射帶來靈活性的同時,也大大降低了應用程序的效率。本文將利用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值進行的。所以會更快。完整的代碼可以從這里下載到。