C# 快速動態建立物件實體技巧

  • 4140
  • 0

前陣子看到許多朋友轉載這篇關於如何使用C# 快速建立物件實體的文章

 

其中使用 LINQ Expression 動態建立物件的效能令人驚豔,不過引起了我的好奇心,在 C#  中,建立物件的方法大概可以歸類為三種。

1. new 運算子,最快,但只能寫死,頂多只能搭配泛型限制來建立擁有無參數建構子的物件
2. Reflection,反射,最慢,但最簡單
3. Emit,IL Code 寫入,最快,但很麻煩

在該文章中,LINQ Expression 的 New 函式表現的效能相當高,甚至高過使用Emit,這讓我很訝異,在印象中,LINQ Expression 走的是 Emit 模式,其內層實作是把 Expression 轉成 Emit IL Code,就理論上而言,沒道理會比純 Emit IL 快。

在仔細查看 Expression 的實作原始碼後更確立了這點,但事實顯示 Expression 就是比 Emit 快,這該如何解釋呢? 為了找到答案,我用了以下三種建立物件的方式來測試。

(PS: 注意,我刻意使用無參數建構子,事實上在這種情境下,泛型加上 new 是最快的,但其無法擴充至有參數的建構子物件上)

LINQ Expression
public static class LinqBuilderWithoutCachingWithGeneric<T>
{

    private static Dictionary<Type, Func<T>> _cache = new Dictionary<Type, Func<T>>();

    public static T Build()
    {

        if (!_cache.ContainsKey(typeof(T)))
        {
            var t = typeof(T);
            var ex = new Expression[] { Expression.New(t) };
            var block = Expression.Block(t, ex);
            var builder = Expression.Lambda<Func<T>>(block).Compile();
            _cache[typeof(T)] = builder;
        }
        return _cache[typeof(T)]();
    }
}
Reflection
public static class ReflectionOnly<T>
{

    private static Dictionary<Type, ConstructorInfo> _cache = new Dictionary<Type, ConstructorInfo>();

    public static T Build()
    {
        if(!_cache.ContainsKey(typeof(T)))
        {
            _cache[typeof(T)] = typeof(T).GetConstructor(new Type[] { });
        }
        return (T)_cache[typeof(T)].Invoke(null);
    }
}
Emit
public static class MyEmit<T>
{

    private static Dictionary<Type, InvokeMethod> _cache = new Dictionary<Type, InvokeMethod>();
    
    delegate T InvokeMethod();

    public static T Build()
    {
       if (!_cache.ContainsKey(typeof(T)))
       {
           ConstructorInfo emptyConstructor = typeof(T).GetConstructor(Type.EmptyTypes);
           var dynamicMethod = new DynamicMethod("CreateInstance", typeof(T), Type.EmptyTypes, true);
           ILGenerator ilGenerator = dynamicMethod.GetILGenerator();
           ilGenerator.Emit(OpCodes.Newobj, emptyConstructor);
           ilGenerator.Emit(OpCodes.Ret);
           _cache[typeof(T)] = (InvokeMethod)dynamicMethod.CreateDelegate(typeof(InvokeMethod));
       }
       return _cache[typeof(T)]();
    }
}

然後以下面的程式碼進行測試。

class MyClass
{
    public string TestProp { get; set; }
}


class Program
{
    static void Benchmark(Action func, string label)
    {
        var sw = new Stopwatch();
        sw.Start();
        func();
        sw.Stop();
        Console.WriteLine($"{label} elapsed {sw.ElapsedMilliseconds} ms");
    }

    static void TestLinq()
    {
        for(int i = 0; i < 10000000; i++)
        {
            var o = LinqBuilderWithoutCachingWithGeneric<MyClass>.Build();
        }
    }

    static void TestEmit()
    {
        for (int i = 0; i < 10000000; i++)
        {
             var o = MyEmit<MyClass>.Build();
        }
    }

    static void TestReflection()
    {
        for (int i = 0; i < 10000000; i++)
        {
             var o = ReflectionOnly<MyClass>.Build();
        }
    }

    static void Main(string[] args)
    {

            Benchmark(TestLinq, "Linq");

            Benchmark(TestEmit, "Emit");

            Benchmark(TestReflection, "Reflection");

            Benchmark(TestLinq, "Linq");

            Benchmark(TestEmit, "Emit");

            Benchmark(TestReflection, "Reflection");

            Benchmark(TestLinq, "Linq");

            Benchmark(TestEmit, "Emit");

            Benchmark(TestReflection, "Reflection");

            Console.ReadLine();

     }
}

結果如下:

就如同該文章提及的,Emit 與  LINQ Expression 的差異很小,但可以明顯觀察到 LINQ Expression 略快於 Emit,雖不合理,但事實是如此沒錯。只是我還是很好奇其中的差異到底在哪裡,在更深入的審視 Expression  原始碼後發現,

他走了另外一條完全不同的道路,完全從 TypeBuilder 開始建立整個動態 IL 鏈,而不是使用 DynamicMethod 物件來建立 Delegate,所以可以判定 DynamicMethod 是這個差異的關鍵物件,把研究重心轉向 DynamicMethod 後發現,

其實他跟 Expression 的實作差不多,只是有個 Security 的小設計,看來是這個小設計拉慢了速度,在把 Emit IL 的程式碼改為跳過 Security 之後,結果如下:

Emit
public static class MyEmit<T>
{

     private static Dictionary<Type, InvokeMethod> _cache = new Dictionary<Type, InvokeMethod>();
     delegate T InvokeMethod();
     public static T Build()
     {
        if (!_cache.ContainsKey(typeof(T)))
        {
            ConstructorInfo emptyConstructor = typeof(T).GetConstructor(Type.EmptyTypes);
            var dynamicMethod = new DynamicMethod("CreateInstance", typeof(T), Type.EmptyTypes, typeof(MyEmit<T>));
            ILGenerator ilGenerator = dynamicMethod.GetILGenerator();
            ilGenerator.Emit(OpCodes.Newobj, emptyConstructor);
            ilGenerator.Emit(OpCodes.Ret);
            _cache[typeof(T)] = (InvokeMethod)dynamicMethod.CreateDelegate(typeof(InvokeMethod));
        }
        return _cache[typeof(T)]();
     }
}

 

答案揭曉,Emit IL 仍然是效能之王。

不過,Expression 的可讀性高太多了,如果沒有很高的效能考量需求,選 Expression 才是正解。

Code