[C#] 使用 Expression Tree 建立快速複製代理
最近在寫工具,因為會去作壓測,確保資料量大時的執行時間
這時就會很重視執行時間,希望能盡可能的減少一些頻繁工作的執行時間
所以開始學習 Emit 及 Expression Tree,前者能玩的花招比較多,但是相對不直覺,需要有 IL 的底才行,而後者則相對容易一些。
而這次作的任務是希望可以作一個複製指定 Model 到另一個新實體上(就近似於 clone ),只是複製的 source 及 target 並不是相同類別,而是可能 target 的類別是繼承 source 的類別的,並且希望如果是物件的話,嘗試 new 出另一個新的物件建立並複製屬性值。
以下將上對應的程式碼
public static Action<TSource, TTarget> FastCloneProxy<TSource, TTarget>() { Type tsource = typeof(TSource); Type ttarget = typeof(TTarget); var psource = Expression.Parameter(tsource, "source"); var ptarget = Expression.Parameter(ttarget, "target"); var expressions = new List<Expression>(); if (tsource.IsAssignableFrom(ttarget)) { foreach (var p in tsource.GetProperties()) { var prop = ttarget.GetProperty(p.Name); AppendPropertyProcess(p, prop, expressions, psource, ptarget); } } else if (ttarget.IsAssignableFrom(tsource)) { foreach (var p in ttarget.GetProperties()) { var prop = tsource.GetProperty(p.Name); AppendPropertyProcess(prop, p, expressions, psource, ptarget); } } else throw new InvalidOperationException("TSource 與 TTarget 不存在階層關聯。"); var exp = Expression.Lambda<Action<TSource, TTarget>>(Expression.Block(expressions.ToArray()), psource, ptarget); return exp.Compile(); } private static Expression FastCloneProxyNested(Expression source, ConstructorInfo constructor, int deepLimit = 10, int index = 0) { if (constructor == null) throw new ArgumentNullException(nameof(constructor)); if (constructor.GetParameters().Length > 0) throw new InvalidOperationException("建構子參數需為 0。"); var targetType = constructor.DeclaringType; if (source.Type.IsAssignableFrom(targetType)) { if (deepLimit > 0 && index >= deepLimit) return source; List<Expression> expressions = new List<Expression>(); var target = Expression.Parameter(targetType); expressions.Add(Expression.Assign(target, Expression.New(constructor))); foreach (var p in source.Type.GetProperties()) { var prop = targetType.GetProperty(p.Name); AppendPropertyProcess(p, prop, expressions, source, target, deepLimit, index); } expressions.Add(target); return Expression.Block(new ParameterExpression[] { target }, expressions); } else if (targetType.IsAssignableFrom(source.Type)) { if (deepLimit > 0 && index >= deepLimit) return Expression.Default(targetType); List<Expression> expressions = new List<Expression>(); var target = Expression.Parameter(targetType); expressions.Add(Expression.Assign(target, Expression.New(constructor))); foreach (var p in targetType.GetProperties()) { var prop = source.Type.GetProperty(p.Name); AppendPropertyProcess(prop, p, expressions, source, target, deepLimit, index); } expressions.Add(target); return Expression.Block(new ParameterExpression[] { target }, expressions); } else throw new InvalidOperationException("source 與 target 型別不存在階層關聯。"); } private static void AppendPropertyProcess(PropertyInfo sourcePropertyInfo, PropertyInfo targetPropertyInfo, List<Expression> expressions, Expression source, Expression target, int deepLimit = 10, int index = 0) { if (sourcePropertyInfo != null && targetPropertyInfo != null && sourcePropertyInfo.CanRead && targetPropertyInfo.CanWrite) { if (IsSimpleType(sourcePropertyInfo.PropertyType)) expressions.Add(Expression.Assign(Expression.Property(target, sourcePropertyInfo.Name), Expression.Property(source, sourcePropertyInfo.Name))); else { var constructor2 = targetPropertyInfo.PropertyType.GetConstructor(Type.EmptyTypes); if (constructor2 == null) expressions.Add(Expression.Assign(Expression.Property(target, sourcePropertyInfo.Name), Expression.Property(source, sourcePropertyInfo.Name))); else { var nullcheck = Expression.NotEqual(Expression.Property(source, sourcePropertyInfo.Name), Expression.Constant(null, typeof(object))); expressions.Add(Expression.IfThen( nullcheck, Expression.Assign(Expression.Property(target, sourcePropertyInfo.Name), FastCloneProxyNested(Expression.Property(source, sourcePropertyInfo.Name), constructor2, deepLimit, index + 1)) ) ); } } } } private static bool IsSimpleType(Type type) => type.IsPrimitive || new Type[] { typeof(string), typeof(decimal), typeof(DateTime), typeof(DateTimeOffset), typeof(TimeSpan), typeof(Guid) }.Contains(type) || type.IsEnum || (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>) && IsSimpleType(type.GetGenericArguments()[0]));
以上程式碼可以建立快速的代理語法,後續對指定兩種型別的複製就可以呼叫建立的 Action 來執行複製值的動作了。
後面補上之前跑的一些 Benchmark 的評測,下述評測是使用 Expression Tree, Emit, MethodInfo.Invoke, Type.InvokeMember 分別呼叫一個空函式:
下圖主要是比較 Emit 及 Expression Tree 作對應函式的呼叫耗費的時間
Expression Tree:
var instance = Expression.Parameter(type);
return Expression.Lambda<Action<Test>>(Expression.Call(instance, method), instance).Compile();
Emit
var m = new DynamicMethod("invoker", null, new Type[] { type }, true);
var il = m.GetILGenerator();
il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Call, method);
il.Emit(OpCodes.Ret);
return (Action<Test>)m.CreateDelegate(typeof(Action<Test>));
可以看到在編譯快速叫用的函式上,是 Emit 耗費極短的時間,但如果有 Cache 編譯後的函式,這個編譯也只會發生 1 次或極少次數才對。
而在上述編譯的函式呼叫,及其它兩者的呼叫方式的評比則如下
結論:
理論上 Emit 與 Expression Tree 的叫用時間應該極為相近才對,並且理論上 Emit 應該會擁有不輸於 Expression Tree 的執行時間才對,但我不確定為何我跑起來反而 Expression Tree 卻是比較快的,但兩者應該算是在誤差範圍中。
然後不論是 Emit 還是 Expression Tree ,動態叫用函式的執行時間都遠優於 Method.Invoke ,但是加上編譯時間後則並非如此,所以需要注意,當要動態叫用的函式極少被呼叫時,也可以考慮直接用 Reflection 就行,但如果有把編譯後的方法給 Cache 起來的話,執行次數一多時, Expression Tree 與 Emit 的效能將遠優於 Reflection ,可以為一些壓力測試取得良好的成績。
留言
張貼留言