[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 次或極少次數才對。
而在上述編譯的函式呼叫,及其它兩者的呼叫方式的評比則如下
可以看到直接呼叫函式速度仍然是最快的,後續則是 Expression Tree, Emit, Method.Invoke 及 Type.InvokeMember 這樣的順序。

結論:
理論上 Emit 與 Expression Tree 的叫用時間應該極為相近才對,並且理論上 Emit 應該會擁有不輸於 Expression Tree 的執行時間才對,但我不確定為何我跑起來反而 Expression Tree 卻是比較快的,但兩者應該算是在誤差範圍中。

然後不論是 Emit 還是 Expression Tree ,動態叫用函式的執行時間都遠優於 Method.Invoke ,但是加上編譯時間後則並非如此,所以需要注意,當要動態叫用的函式極少被呼叫時,也可以考慮直接用 Reflection 就行,但如果有把編譯後的方法給 Cache 起來的話,執行次數一多時, Expression Tree 與 Emit 的效能將遠優於 Reflection ,可以為一些壓力測試取得良好的成績。



留言

這個網誌中的熱門文章

DB 資料庫呈現復原中

Outlook 刪除大量重覆信件

[VB.Net] If vs IIf ,兩者的差異