閱讀657 返回首頁    go 阿裏雲 go 技術社區[雲棲]


關於Expression Tree和IL Emit的所謂的"性能差別"

昨天寫了《三種屬性操作性能比較》,有個網友寫信問我一個問題:從性能上看,Expression Tree和IL Emit孰優孰劣?雖然我在回信中作了簡單的回答,但不知道這個網友是否懂我的意思。反正今天呆在家裏也沒事兒,幹脆再就這個話題再寫一篇文章。

目錄:
一、Expression Tree和IL Emit並不存在所謂的性能差異
二、屬性賦值操作的兩種寫法
三、屬性取值操作的兩種寫法
四、兩種寫法對應的IL

 

Expression Tree和IL Emit的性能孰優孰劣,這本是個“不是問題的問題”。因為兩者之間並不存在本質的區別,所以也談不上性能的優劣問題。舉個例子來說,我們知道.NET Framework 2.0,3.0和3.5使用的是相同的CLR。但是C# 3.0、3.5在2.0的基礎上推出了很多語言層麵的特性,比如自動實現屬性:

   1: public class Foo
   2: {
   3:     public Bar Bar{get;set;}
   4:     public Foo()
   5:     {
   6:         this.Bar = new Bar();
   7:     }
   8: }

我們也可以按照下麵“傳統”的方式來寫上麵這段代碼,誰都知道這兩種寫法在本質上是完全一樣的。就上麵的程序來說,在編譯的時候C#編譯器會將其轉化成下一種形式,什麼自動實現屬性、匿名屬性、擴展方法,都是浮雲——語法糖而已。

   1: public class Foo
   2: {
   3:     private Bar _bar;
   4:     public Bar Bar
   5:     {
   6:         get{return _bar;}
   7:         set{_bar = value;}
   8:     }
   9:     public Foo()
  10:     {
  11:         _bar = new Bar();
  12:     }
  13: }

Expression Tree和IL Emit之間的關係與這些“語法糖”類似。編譯後的Expression Tree就是IL代碼;而IL Emit讓我們可以用高級語言的編程方式來控製中間語言(IL)程序。由於最終的東西都是一樣的,談不上誰比誰好的問題。編譯Expression Tree實現了向IL的轉換,如果你通過IL Emit寫的IL能夠比Expression Tree自動轉換的好,那麼你的程序性能就好,否則性能就差。但是我們不能說Expression Tree和IL Emit在性能上孰優孰劣。

我們說明Expression Tree和IL Emit之間不存在性能的差異,我們不妨寫個例子。簡單起見,我們還是采用前麵談到過的屬性賦值和取值的操作為例。假設有如下一個接口IFoo,包含一個類型和名稱均為Bar的可讀寫的屬性。

   1: public interface IFoo
   2: {
   3:     Bar{get;set;}
   4: }
   5: public class Bar{}

現在我們通過Expression Tree和IL Emit兩種方式編寫一個靜態方法對IFoo對象的Bar屬性進行賦值。簡單起見,我們甚至將靜態方法的參數類型直接指定為IFoo和Bar,從而省去了類型轉換操作。下麵是通過Expression Tree進行屬性賦值的方法:SetPropertyValueViaExpression。

   1: public static void SetPropertyValueViaExpression(IFoo foo, Bar bar)
   2: {
   3:     var property = typeof(IFoo).GetProperty("Bar");
   4:     var target = Expression.Parameter(typeof(IFoo));
   5:     var propertyValue = Expression.Parameter(typeof(Bar));
   6:     var setPropertyValue = Expression.Call(target, property.GetSetMethod(), propertyValue);
   7:     var setAction= Expression.Lambda<Action<IFoo, Bar>>(setPropertyValue, target, propertyValue).Compile();
   8:     setAction(foo, bar);
   9: }

而下麵的SetPropertyValueViaEmit則通過IL Emit的方式完成了一樣的工作:

   1: public static void SetPropertyValueViaEmit(IFoo foo, Bar bar)
   2: {
   3:     var property = typeof(IFoo).GetProperty("Bar");
   4:     DynamicMethod method = new DynamicMethod("SetValue", null, new Type[] { typeof(IFoo), typeof(Bar) });
   5:     ILGenerator ilGenerator = method.GetILGenerator();
   6:     ilGenerator.Emit(OpCodes.Ldarg_0);
   7:     ilGenerator.Emit(OpCodes.Ldarg_1);
   8:     ilGenerator.EmitCall(OpCodes.Callvirt, property.GetSetMethod(), null);
   9:     ilGenerator.Emit(OpCodes.Ret);
  10:  
  11:     method.DefineParameter(1, ParameterAttributes.In, "obj");
  12:     method.DefineParameter(2, ParameterAttributes.In, "value");
  13:     var setAction = (Action<IFoo, Bar>)method.CreateDelegate(typeof(Action<IFoo, Bar>));
  14:     setAction(foo, bar);
  15: }

接下來,我們來編寫用於進行屬性取值操作的方法。下麵的SetPropertyValueViaExpression方法是基於Expression Tree的。

   1: public static Bar GetPropertyValueViaExpression(IFoo foo)
   2: {
   3:     var property = typeof(IFoo).GetProperty("Bar");
   4:     var target = Expression.Parameter(typeof(IFoo));
   5:     var getPropertyValue = Expression.Property(target, property);
   6:     var getFunc = Expression.Lambda<Func<IFoo, Bar>>(getPropertyValue, target).Compile();
   7:     return getFunc(foo);
   8: }

下麵則是基於IL Emit的版本:

   1: public static Bar GetPropertyValueViaEmit(IFoo foo)
   2: {
   3:     var property = typeof(IFoo).GetProperty("Bar");
   4:     DynamicMethod method = new DynamicMethod("GetValue", typeof(Bar), new Type[] { typeof(IFoo) });
   5:  
   6:     ILGenerator ilGenerator = method.GetILGenerator();
   7:     ilGenerator.Emit(OpCodes.Ldarg_0);
   8:     ilGenerator.EmitCall(OpCodes.Callvirt, property.GetGetMethod(), null);
   9:     ilGenerator.Emit(OpCodes.Ret);
  10:  
  11:     method.DefineParameter(1, ParameterAttributes.In, "target");
  12:     var getFunc = (Func<IFoo, Bar>)method.CreateDelegate(typeof(Func<IFoo, Bar>));
  13:     return getFunc(foo);
  14: }

我們說過,經過編譯的Expression Tree就是一段IL代碼,而IL Emit則直接反映了IL的執行流程。要判斷兩者在性能方麵孰優孰劣,我們隻需要看看Expression Tree最終被轉換成怎樣的IL。我們現在的做法是動態生成一個程序集,將Expression Tree部分定義到一個方法之中。雖然IL Emit已經是真實底反映了底層的IL代碼,但是為了我們的比較更加直觀,我們也將IL Emit的部分也寫入相應的方法。

為此我們在一個Console應用中的Main方法編寫了如下的代碼:動態創建了名稱為Artech.EmitVsExpression的程序集,其中定義了同名的模塊。一個唯一的類型Program定義其中,其中定義了四個靜態方法:GetPropertyValueViaExpression、SetPropertyValueViaExpression、GetPropertyValueViaEmit和GetPropertyValueViaEmit。而方法體部分則是上麵Expression Tree和IL Emit定義的內容。最後這個程序集被保存為一個同名的.dll文件。

   1: static void Main()
   2: {
   3:     var property        = typeof(IFoo).GetProperty("Bar");
   4:     var assemblyBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly(new AssemblyName("Artech.EmitVsExpression"), AssemblyBuilderAccess.RunAndSave);
   5:     var moduleBuilder   = assemblyBuilder.DefineDynamicModule("Artech.EmitVsExpression", "Artech.EmitVsExpression.dll");
   6:     var typeBuilder     = moduleBuilder.DefineType("Program");
   7:  
   8:     //GetPropertyValueViaExpression
   9:     var methodBuilder       = typeBuilder.DefineMethod("GetPropertyValueViaExpression", MethodAttributes.Static | MethodAttributes.Public, typeof(Bar), new Type[] { typeof(IFoo) });
  10:     var target              = Expression.Parameter(typeof(IFoo));
  11:     var getPropertyValue    = Expression.Property(target, property);
  12:     Expression.Lambda<Func<IFoo, Bar>>(getPropertyValue, target).CompileToMethod(methodBuilder);
  13:  
  14:     //SetPropertyValueViaExpression
  15:     methodBuilder           = typeBuilder.DefineMethod("SetPropertyValueViaExpression", MethodAttributes.Static | MethodAttributes.Public, typeof(void), new Type[] { typeof(IFoo), typeof(Bar) });
  16:     target                  = Expression.Parameter(typeof(IFoo));
  17:     var propertyValue       = Expression.Parameter(typeof(Bar));
  18:     var setPropertyValue    = Expression.Call(target, property.GetSetMethod(), propertyValue);
  19:     Expression.Lambda<Action<IFoo, Bar>>(setPropertyValue, target, propertyValue).CompileToMethod(methodBuilder);
  20:  
  21:     //GetPropertyValueViaEmit
  22:     methodBuilder           = typeBuilder.DefineMethod("GetPropertyValueViaEmit", MethodAttributes.Static| MethodAttributes.Public, typeof(Bar), new Type[] { typeof(IFoo) });
  23:     ILGenerator ilGenerator = methodBuilder.GetILGenerator();
  24:     ilGenerator.Emit(OpCodes.Ldarg_0);
  25:     ilGenerator.EmitCall(OpCodes.Callvirt, property.GetGetMethod(), null);
  26:     ilGenerator.Emit(OpCodes.Ret);
  27:  
  28:     //SetPropertyValueViaEmit
  29:     methodBuilder   = typeBuilder.DefineMethod("SetPropertyValueViaEmit", MethodAttributes.Static | MethodAttributes.Public, typeof(void), new Type[] { typeof(IFoo), typeof(Bar) });
  30:     ilGenerator     = methodBuilder.GetILGenerator();
  31:     ilGenerator.Emit(OpCodes.Ldarg_0);
  32:     ilGenerator.Emit(OpCodes.Ldarg_1);
  33:     ilGenerator.EmitCall(OpCodes.Callvirt, property.GetSetMethod(), null);
  34:     ilGenerator.Emit(OpCodes.Ret);
  35:  
  36:     typeBuilder.CreateType();
  37:     assemblyBuilder.Save("Artech.EmitVsExpression.dll");
  38: }

現在我們通過IL Disassembler打開這個.dll文件,看看四個靜態方法的IL代碼。下麵是用於用於獲取屬性值的GetPropertyValueViaExpression和GetPropertyValueViaEmit方法,我們可以看出它們具有完全一致的方式體。

   1: .method public static class [EmitVsExpressionTree]Bar 
   2:         GetPropertyValueViaExpression(class [EmitVsExpressionTree]IFoo A_0) cil managed
   3: {
   4:   // Code size       7 (0x7)
   5:   .maxstack  1
   6:   IL_0000:  ldarg.0
   7:   IL_0001:  callvirt   instance class [EmitVsExpressionTree]Bar [EmitVsExpressionTree]IFoo::get_Bar()
   8:   IL_0006:  ret
   9: } // end of method Program::GetPropertyValueViaExpression
  10:  
  11: .method public static class [EmitVsExpressionTree]Bar 
  12:         GetPropertyValueViaEmit(class [EmitVsExpressionTree]IFoo A_0) cil managed
  13: {
  14:   // Code size       7 (0x7)
  15:   .maxstack  1
  16:   IL_0000:  ldarg.0
  17:   IL_0001:  callvirt   instance class [EmitVsExpressionTree]Bar [EmitVsExpressionTree]IFoo::get_Bar()
  18:   IL_0006:  ret
  19: } // end of method Program::GetPropertyValueViaEmit

下麵是用於對屬性進行賦值的兩個靜態方法:SetPropertyValueViaExpression和SetPropertyValueViaEmit,毫無疑問它們之間也沒有差異。到現在,你還在懷疑兩種之間在性能上孰優孰劣嗎?

   1: .method public static void  SetPropertyValueViaExpression(class [EmitVsExpressionTree]IFoo A_0,
   2:                                                           class [EmitVsExpressionTree]Bar A_1) cil managed
   3: {
   4:   // Code size       8 (0x8)
   5:   .maxstack  2
   6:   IL_0000:  ldarg.0
   7:   IL_0001:  ldarg.1
   8:   IL_0002:  callvirt   instance void [EmitVsExpressionTree]IFoo::set_Bar(class [EmitVsExpressionTree]Bar)
   9:   IL_0007:  ret
  10: } // end of method Program::SetPropertyValueViaExpression
  11:  
  12: .method public static void  SetPropertyValueViaEmit(class [EmitVsExpressionTree]IFoo A_0,
  13:                                                     class [EmitVsExpressionTree]Bar A_1) cil managed
  14: {
  15:   // Code size       8 (0x8)
  16:   .maxstack  2
  17:   IL_0000:  ldarg.0
  18:   IL_0001:  ldarg.1
  19:   IL_0002:  callvirt   instance void [EmitVsExpressionTree]IFoo::set_Bar(class [EmitVsExpressionTree]Bar)
  20:   IL_0007:  ret
  21: } // end of method Program::SetPropertyValueViaEmit

既然在IL上它們沒有差別,那麼它們就是兩對等效的方法。如果你通過Reflector來打開我們生成的.dll,你會清晰地看到這真的是兩對完全一致的方法。

   1: internal class Program
   2: {
   3:     // Methods
   4:     public static Bar GetPropertyValueViaEmit(IFoo foo1)
   5:     {
   6:         return foo1.Bar;
   7:     }
   8:  
   9:     public static Bar GetPropertyValueViaExpression(IFoo foo1)
  10:     {
  11:         return foo1.Bar;
  12:     }
  13:  
  14:     public static void SetPropertyValueViaEmit(IFoo foo1, Bar bar1)
  15:     {
  16:         foo1.Bar = bar1;
  17:     }
  18:  
  19:     public static void SetPropertyValueViaExpression(IFoo foo1, Bar bar1)
  20:     {
  21:         foo1.Bar = bar1;
  22:     }
  23: } 

晚綁定場景下對象屬性賦值和取值可以不需要PropertyInfo
三種屬性操作性能比較:PropertyInfo + Expression Tree + Delegate.CreateDelegate
關於Expression Tree和IL Emit的所謂的"性能差別"


作者:蔣金楠
微信公眾賬號:大內老A
微博:www.weibo.com/artech
如果你想及時得到個人撰寫文章以及著作的消息推送,或者想看看個人推薦的技術資料,可以掃描左邊二維碼(或者長按識別二維碼)關注個人公眾號(原來公眾帳號蔣金楠的自媒體將會停用)。
本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁麵明顯位置給出原文連接,否則保留追究法律責任的權利。
原文鏈接

最後更新:2017-10-27 11:04:32

  上一篇:go  小編給各位小夥伴分享一款OSS費用統計工具
  下一篇:go  詳解Twitter開源分布式自增ID算法snowflake,附演算驗證過程