再看ExpressionTree,Emit,反射建立物件效能對比
【前言】
前幾日心血來潮想研究著做一個Spring框架,自然地就涉及到了Ioc容器物件建立的問題,研究怎麼高效能地建立一個物件。第一聯想到了Emit,興致沖沖寫了個Emit建立物件的工廠。在做效能測試的時候,發現居然比反射Activator.CreateInstance方法建立物件毫無優勢可言。繼而又寫了個Expression Tree的物件工廠,發現和Emit不相上下,比起系統反射方法仍然無優勢可言。
第一時間查看了園內大神們的研究,例如:
Leven的 ofollow,noindex" target="_blank">探究.net物件的建立,質疑《再談Activator.CreateInstance(Type type)方法建立物件和Expression Tree建立物件效能的比較》
Will Meng的 再談Activator.CreateInstance(Type type)方法建立物件和Expression Tree建立物件效能的比較(更新版)
詳細對比了後發現, 上述大佬們的對比都是使用無參建構函式做的效能對比 。
於是,我也用Expression Tree寫了個無參建構函式的Demo,對比之下發現, 無參構造Expression Tree實現方式確實比反射Activator.CreateInstance方法效能要高很多,但是如果想要相容帶參的物件建立,在引數判斷,方法快取上來說,耗費了很多的時間,效能並不比直接反射呼叫好 ,下面放出測試的程式碼,歡迎博友探討,雅正。
【實現功能】
我們要實現一個建立物件的工廠。
new物件,Expression Tree實現(引數/不考慮引數),Emit+Delegate(考慮引數)實現方式做對比。
【實現過程】
準備好測試的物件:
準備兩個類,ClassA,ClassB,其中ClassA有ClassB的引數構造,ClassB無參構造。
1 public class ClassA 2 { 3public ClassA(ClassB classB) { } 4public int GetInt() => default(int); 5 } 6 public class ClassB 7 { 8public int GetInt() => default(int); 9 }
1.最簡單不考慮引數的 Expression Tree方式建立物件(無帶參建構函式)
1public class ExpressionCreateObject 2{ 3private static Func<object> func; 4public static T CreateInstance<T>() where T : class 5{ 6if (func == null) 7{ 8var newExpression = Expression.New(typeof(T)); 9func = Expression.Lambda<Func<object>>(newExpression).Compile(); 10} 11return func() as T; 12} 13}
2.有引數處理的Expression Tree方式建立物件(帶參建構函式,且針對引數的委託進行了本地快取)
1public class ExpressionCreateObjectFactory 2{ 3private static Dictionary<string, Func<object[], object>> funcDic = new Dictionary<string, Func<object[], object>>(); 4public static T CreateInstance<T>() where T : class 5{ 6return CreateInstance(typeof(T), null) as T; 7} 8 9public static T CreateInstance<T>(params object[] parameters) where T : class 10{ 11return CreateInstance(typeof(T), parameters) as T; 12} 13 14static Expression[] buildParameters(Type[] parameterTypes, ParameterExpression paramExp) 15{ 16List<Expression> list = new List<Expression>(); 17for (int i = 0; i < parameterTypes.Length; i++) 18{ 19//從引數表示式(引數是:object[])中取出引數 20var arg = BinaryExpression.ArrayIndex(paramExp, Expression.Constant(i)); 21//把引數轉化成指定型別 22var argCast = Expression.Convert(arg, parameterTypes[i]); 23 24list.Add(argCast); 25} 26return list.ToArray(); 27} 28 29public static object CreateInstance(Type instanceType, params object[] parameters) 30{ 31 32Type[] ptypes = new Type[0]; 33string key = instanceType.FullName; 34 35if (parameters != null && parameters.Any()) 36{ 37ptypes = parameters.Select(t => t.GetType()).ToArray(); 38key = string.Concat(key, "_", string.Concat(ptypes.Select(t => t.Name))); 39} 40 41if (!funcDic.ContainsKey(key)) 42{ 43ConstructorInfo constructorInfo = instanceType.GetConstructor(ptypes); 44 45//建立lambda表示式的引數 46var lambdaParam = Expression.Parameter(typeof(object[]), "_args"); 47 48//建立建構函式的引數表示式陣列 49var constructorParam = buildParameters(ptypes, lambdaParam); 50 51var newExpression = Expression.New(constructorInfo, constructorParam); 52 53funcDic.Add(key, Expression.Lambda<Func<object[], object>>(newExpression, lambdaParam).Compile()); 54} 55return funcDic[key](parameters); 56} 57}
3.有引數處理的 Emit+Delegate 方式建立物件(帶參建構函式,且針對引數Delegate本地快取)
1 namespace SevenTiny.Bantina 2 { 3internal delegate object CreateInstanceHandler(object[] parameters); 4 5public class CreateObjectFactory 6{ 7static Dictionary<string, CreateInstanceHandler> mHandlers = new Dictionary<string, CreateInstanceHandler>(); 8 9public static T CreateInstance<T>() where T : class 10{ 11return CreateInstance<T>(null); 12} 13 14public static T CreateInstance<T>(params object[] parameters) where T : class 15{ 16return (T)CreateInstance(typeof(T), parameters); 17} 18 19public static object CreateInstance(Type instanceType, params object[] parameters) 20{ 21Type[] ptypes = new Type[0]; 22string key = instanceType.FullName; 23 24if (parameters != null && parameters.Any()) 25{ 26ptypes = parameters.Select(t => t.GetType()).ToArray(); 27key = string.Concat(key, "_", string.Concat(ptypes.Select(t => t.Name))); 28} 29 30if (!mHandlers.ContainsKey(key)) 31{ 32CreateHandler(instanceType, key, ptypes); 33} 34return mHandlers[key](parameters); 35} 36 37static void CreateHandler(Type objtype, string key, Type[] ptypes) 38{ 39lock (typeof(CreateObjectFactory)) 40{ 41if (!mHandlers.ContainsKey(key)) 42{ 43DynamicMethod dm = new DynamicMethod(key, typeof(object), new Type[] { typeof(object[]) }, typeof(CreateObjectFactory).Module); 44ILGenerator il = dm.GetILGenerator(); 45ConstructorInfo cons = objtype.GetConstructor(ptypes); 46 47if (cons == null) 48{ 49throw new MissingMethodException("The constructor for the corresponding parameter was not found"); 50} 51 52il.Emit(OpCodes.Nop); 53 54for (int i = 0; i < ptypes.Length; i++) 55{ 56il.Emit(OpCodes.Ldarg_0); 57il.Emit(OpCodes.Ldc_I4, i); 58il.Emit(OpCodes.Ldelem_Ref); 59if (ptypes[i].IsValueType) 60il.Emit(OpCodes.Unbox_Any, ptypes[i]); 61else 62il.Emit(OpCodes.Castclass, ptypes[i]); 63} 64 65il.Emit(OpCodes.Newobj, cons); 66il.Emit(OpCodes.Ret); 67CreateInstanceHandler ci = (CreateInstanceHandler)dm.CreateDelegate(typeof(CreateInstanceHandler)); 68mHandlers.Add(key, ci); 69} 70} 71} 72} 73 }
【系統測試】
我們編寫單元測試程式碼對上述幾個程式碼段進行效能測試:
1.無參建構函式的單元測試
1 [Theory] 2 [InlineData(1000000)] 3 [Trait("description", "無參構造各方法呼叫效能對比")] 4 public void PerformanceReportWithNoArguments(int count) 5 { 6Trace.WriteLine($"#{count} 次呼叫:"); 7 8double time = StopwatchHelper.Caculate(count, () => 9{ 10ClassB b = new ClassB(); 11}).TotalMilliseconds; 12Trace.WriteLine($"‘New’耗時 {time} milliseconds"); 13 14double time2 = StopwatchHelper.Caculate(count, () => 15{ 16ClassB b = CreateObjectFactory.CreateInstance<ClassB>(); 17}).TotalMilliseconds; 18Trace.WriteLine($"‘Emit 工廠’耗時 {time2} milliseconds"); 19 20double time3 = StopwatchHelper.Caculate(count, () => 21{ 22ClassB b = ExpressionCreateObject.CreateInstance<ClassB>(); 23}).TotalMilliseconds; 24Trace.WriteLine($"‘Expression’耗時 {time3} milliseconds"); 25 26double time4 = StopwatchHelper.Caculate(count, () => 27{ 28ClassB b = ExpressionCreateObjectFactory.CreateInstance<ClassB>(); 29}).TotalMilliseconds; 30Trace.WriteLine($"‘Expression 工廠’耗時 {time4} milliseconds"); 31 32double time5 = StopwatchHelper.Caculate(count, () => 33{ 34ClassB b = Activator.CreateInstance<ClassB>(); 35//ClassB b = Activator.CreateInstance(typeof(ClassB)) as ClassB; 36}).TotalMilliseconds; 37Trace.WriteLine($"‘Activator.CreateInstance’耗時 {time5} milliseconds"); 38 39 40/** 41#1000000 次呼叫: 42‘New’耗時 21.7474 milliseconds 43‘Emit 工廠’耗時 174.088 milliseconds 44‘Expression’耗時 42.9405 milliseconds 45‘Expression 工廠’耗時 162.548 milliseconds 46‘Activator.CreateInstance’耗時 67.3712 milliseconds 47* */ 48 }
通過上面程式碼測試可以看出,100萬次呼叫,相比直接New物件,Expression無引數考慮的實現方式效能最高,比系統反射Activator.CreateInstance的方法效能要高。
這裡沒有提供Emit無參的方式實現,看這個效能測試的結果,預估Emit無參的實現方式效能會比系統反射的效能要高的。
2.帶參建構函式的單元測試
1 [Theory] 2 [InlineData(1000000)] 3 [Trait("description", "帶參構造各方法呼叫效能對比")] 4 public void PerformanceReportWithArguments(int count) 5 { 6Trace.WriteLine($"#{count} 次呼叫:"); 7 8double time = StopwatchHelper.Caculate(count, () => 9{ 10ClassA a = new ClassA(new ClassB()); 11}).TotalMilliseconds; 12Trace.WriteLine($"‘New’耗時 {time} milliseconds"); 13 14double time2 = StopwatchHelper.Caculate(count, () => 15{ 16ClassA a = CreateObjectFactory.CreateInstance<ClassA>(new ClassB()); 17}).TotalMilliseconds; 18Trace.WriteLine($"‘Emit 工廠’耗時 {time2} milliseconds"); 19 20double time4 = StopwatchHelper.Caculate(count, () => 21{ 22ClassA a = ExpressionCreateObjectFactory.CreateInstance<ClassA>(new ClassB()); 23}).TotalMilliseconds; 24Trace.WriteLine($"‘Expression 工廠’耗時 {time4} milliseconds"); 25 26double time5 = StopwatchHelper.Caculate(count, () => 27{ 28ClassA a = Activator.CreateInstance(typeof(ClassA), new ClassB()) as ClassA; 29}).TotalMilliseconds; 30Trace.WriteLine($"‘Activator.CreateInstance’耗時 {time5} milliseconds"); 31 32 33/** 34#1000000 次呼叫: 35‘New’耗時 29.3612 milliseconds 36‘Emit 工廠’耗時 634.2714 milliseconds 37‘Expression 工廠’耗時 620.2489 milliseconds 38‘Activator.CreateInstance’耗時 588.0409 milliseconds 39* */ 40 }
通過上面程式碼測試可以看出,100萬次呼叫,相比直接New物件,系統反射Activator.CreateInstance的方法效能最高,而Emit實現和ExpressionTree的實現方法就要遜色一籌。
【總結】
通過本文的測試,對反射建立物件的效能有了重新的認識,在.netframework低版本中,反射的效能是沒有現在這麼高的,但是經過微軟的迭代升級,目前最新版本的反射呼叫效能還是比較客觀的,尤其是突出在了針對帶引數建構函式的物件建立上,有機會對內部實現做詳細分析。
無參構造無論是採用Expression Tree快取委託還是Emit直接實現,都無需額外的判斷,也並未使用反射,效能比系統反射要高是可以預見到的。但是加入了各種引數的判斷以及針對不同引數的實現方式的快取之後,效能卻被反射反超,因為引數的判斷以及快取時Key的生成,Map集合的儲存鍵值判斷等都是有耗時的,綜合下來,並不比反射好。
系統的效能瓶頸往往並不在反射或者不反射這些建立物件方法的損耗上,經過測試可以發現,即便使用反射建立,百萬次的呼叫耗時也不到1s,但是百萬次的系統呼叫往往耗時是比較長的,我們做測試的目的僅僅是為了探索,具體在框架的實現中,會著重考慮框架的易用性,容錯性等更為關鍵的部分。
宣告:並不是對園內大佬有啥質疑,個人認為僅僅是對以往測試的一種測試用例的補充,如果對測試過程有任何異議或者優化的部分,歡迎評論區激起波濤~!~~
【原始碼地址】
或者直接clone程式碼檢視專案: https://github.com/sevenTiny/SevenTiny.Bantina