前言 稍微有經驗的.net工程師一定聽過或使用過Reflection,Reflection雖然好用(能動態處理很多事情)但對於效能會有些影響.
我能否擁有Reflection的動態彈性且兼顧效能呢?
有:就是我們這次要介紹的Expression.
我會準備一個範例來比較Expression
和Reflection
效能差異
Expression
Activator.CreateInstance
Activator.CreateInstance Code Activator.CreateInstance
沒甚麼好說就是一個靜態方法傳入Type
動態產生一個物件
Expression code Expression程式碼如下,能發現只是為了建立一個物件需要寫一大堆程式碼(但這些程式碼對於追求效能的你是必須的)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 delegate T Func <T >(params object [] args ) ;class Program { public static Func <T > ExpressionCreator <T >() { ConstructorInfo ctor = typeof (T).GetConstructors().FirstOrDefault(); Type type = ctor.DeclaringType; ParameterInfo[] paramsInfo = ctor.GetParameters(); ParameterExpression param = Expression.Parameter(typeof (object []), "args" ); Expression[] argsExp = new Expression[paramsInfo.Length]; for (int i = 0 ; i < paramsInfo.Length; i++) { Expression index = Expression.Constant(i); Type paramType = paramsInfo[i].ParameterType; Expression paramAccessorExp = Expression.ArrayIndex(param, index); Expression paramCastExp = Expression.Convert(paramAccessorExp, paramType); argsExp[i] = paramCastExp; } NewExpression newExp = Expression.New(ctor, argsExp); LambdaExpression lambda = Expression.Lambda(typeof (Func<T>), newExp, param); return (Func<T>)lambda.Compile(); } }
產生出來Lambda程式碼如下,之後我們就可以透過此lambda來產生我們要物件摟
1 2 3 .Lambda #Lambda1<ConsoleWeb.Func`1[ConsoleWeb.A]>(System.Object[] $args) { .New ConsoleWeb.A() }
BenchmarkDotNet分析 Sample Project放在GitHub ExpressionVsReflection
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 public delegate T Func <T >(params object [] args ) ;public class ObjectProvider { private static ConcurrentDictionary<string ,Delegate> _mapFunc = new ConcurrentDictionary<string , Delegate>(); public static T ReflectionCreator <T >(params object [] args ) where T : class { return Activator.CreateInstance(typeof (T), args ) as T; } public static Func <T > ExpressionCreator <T >() { var key = typeof (T).Name; if (!_mapFunc.TryGetValue(key, out Delegate result)) { ConstructorInfo ctor = typeof (T).GetConstructors().FirstOrDefault(); ParameterInfo[] paramsInfo = ctor.GetParameters(); ParameterExpression param = Expression.Parameter(typeof (object []), "args" ); Expression[] argsExp = new Expression[paramsInfo.Length]; for (int i = 0 ; i < paramsInfo.Length; i++) { Expression index = Expression.Constant(i); Type paramType = paramsInfo[i].ParameterType; Expression paramAccessorExp = Expression.ArrayIndex(param, index); Expression paramCastExp = Expression.Convert(paramAccessorExp, paramType); argsExp[i] = paramCastExp; } NewExpression newExp = Expression.New(ctor, argsExp); LambdaExpression lambda = Expression.Lambda(typeof (Func<T>), newExp, param); result = lambda.Compile(); _mapFunc.GetOrAdd(key, result); } return (Func<T>)result; } }
1 2 3 4 5 6 7 8 9 10 11 12 13 BenchmarkDotNet =v0.12.1 , OS=Windows 10.0 .18363.1440 (1909 /November2018Update/19 H2)Intel Core i7-9700 CPU 3.00GHz, 1 CPU, 8 logical and 8 physical cores [Host] : .NET Framework 4.8 (4.8.4250.0), X86 LegacyJIT DEBUG [AttachedDebugger] ShortRun : .NET Framework 4.8 (4.8.4250.0), X86 LegacyJIT Job-ZMUHMP : .NET Framework 4.8 (4.8.4250.0), X86 LegacyJIT | Method | Mean | StdDev | Error | Gen 0 | Gen 1 | Gen 2 | Allocated | |---------------------------------- |-------------:|----------:|-----------:|-------:|------:|------:|----------:| | & | & | & | &
上面顯示使用Expression效能比起使用Reflection有明顯提升.
Expression小提醒
如果使用Expression或Emit技術時,產生的程式碼(委派)記得使用Cache存放起來,因為如果每次執行都運算Compile效能反而會比Reflection還要更差.
如果沒有使用Cache來執行Expression效率就大大降低甚至比Reflection還要差.
1 2 3 4 5 6 7 8 9 10 | Method | Mean | StdDev | Error | Gen 0 | Gen 1 | Gen 2 | Allocated | |---------------------------------- |-------------: |----------: |------------: |-------: |-------: |------: |----------: | | 'ReflectionCreator no parameter' | 464.3 ns | 5.30 ns | 96.70 ns | 0.0318 | - | - | 168 B | | 'ReflectionCreator had parameter' | 586.2 ns | 0.40 ns | 7.25 ns | 0.0426 | - | - | 224 B | | 'ReflectionCreator had parameter' | 10,000.0 ns | 0.00 ns | NA | - | - | - | - | | 'ReflectionCreator no parameter' | 11,500.0 ns | 0.00 ns | NA | - | - | - | - | | 'ExpressionCreator no parameter' | 45,225.3 ns | 64.40 ns | 1,174.88 ns | 0.4883 | 0.4069 | - | 2909 B | | 'ExpressionCreator had parameter' | 67,297.9 ns | 522.99 ns | 9,541.35 ns | 0.5697 | 0.4883 | - | 3324 B | | 'ExpressionCreator had parameter' | 339,900.0 ns | 0.00 ns | NA | - | - | - | - | | 'ExpressionCreator no parameter' | 352,500.0 ns | 0.00 ns | NA | - | - | - | - |
常用Expression解說 透過上面範例,能發現Expression核心概念是用來產生Delegate程式碼並呼叫使用.
因為動作最小單位是方法 ,委派可以視做方法
Expression.Call 呼叫委派方法
Note:如果呼叫static方法第一個參數是null
Expression.Assign 對於Expression給值 ex: expression1 = expression2
Expression.Block 大括號區域 ex:{}
Expression.Convert 轉型
Expression.Multiply 乘法
Expression.Bind 綁定物件屬性,成員
Expression.MemberInit 建構子成員初始化
(BinaryExpression)
Expression.GreaterThanOrEqual:大於等於
Expression.GreaterThan:大於
Expression.LessThanOrEqual:小於等於
Expression.LessThan:小於
Expression.Lambda 封裝成方法
Expression.New 建立New語法
ArrayAccess vs ArrayIndex
ArrayIndex 是只讀Index
ArrayAccess 可讀可寫
https://stackoverflow.com/questions/14973813/arrayaccess-vs-arrayindex-in-expression-tree
小結 Expression和Emit雖然難寫,但寫的好可以讓程式碼更有彈性且比Reflection更有效能,Expression在許多知名架構都有使用(包含微軟MVC框架也是使用Reflection + Expression來優化),舉一個例子動態代理就很適合使用Expression
或Emit
來優化.
知名動態代理框架castle 有使用到Emit.
Emit想要解決的問題和Expression類似,只是Emit提供更多底層API讓我們呼叫(比Expression可以控制更多細節)
Emit可以寫類似IL程式語法
想必然Emit寫起來也更繁瑣更容易出錯.
所以了解Expression是前往.net進階工程師必經之路.
__此文作者__:Daniel Shih(石頭) __此文地址__: https://isdaniel.github.io/expression-vs-reflection/ __版權聲明__:本博客所有文章除特別聲明外,均採用 CC BY-NC-SA 3.0 TW 許可協議。轉載請註明出處!