[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 ,可以為一些壓力測試取得良好的成績。
留言
張貼留言