C# 泛型編譯特性對效能的影響

2023-11-29 12:00:56

C#作為一種強型別語言,具有豐富的泛型支援,允許開發者編寫可以應對不同資料型別的通用程式碼。然而,在泛型編譯時,針對結構和類作為泛型引數時,會對效能產生不同的影響。

泛型編譯行為

在C#中,泛型編譯行為取決於泛型引數的型別。具體而言,當泛型引數是結構(Struct)時,編譯器會針對每個具體的結構型別生成特定的實現。而當泛型引數是類(Class)時,編譯器則可能生成更通用的實現。

結構 vs 類

結構(Struct)

結構是值型別,它們儲存在棧上,具有較小的記憶體開銷。當泛型引數是結構時,編譯器會針對每個具體的結構型別生成專門的實現,這可能導致更高的效能。因為每個結構型別都有自己的實現,避免了裝箱和拆箱的開銷,同時優化了記憶體分配和存取。

類(Class)

類是參照型別,儲存在堆上,需要通過參照進行存取。當泛型引數是類時,編譯器可能生成更通用的實現。這可能導致較低的效能,因為通用實現需要進行動態排程和參照型別的操作,增加了一些開銷。

測試效能差異

針對不同的泛型引數進行效能測試是一種有效的方法,以觀察結構和類對泛型編譯特性的影響。在測試中,可能會發現對結構型別的泛型引數,其效能可能更高,而對類型別的泛型引數,其效能可能略低。

using System.Diagnostics;

namespace ConsoleApp1 {
    internal interface IValueGetter {
        int GetValue(int index);
    }

    internal class MyTestClass<T> where T : IValueGetter {
        private readonly T _valueGetter;

        public MyTestClass(T valueGetter) {
            _valueGetter = valueGetter;
        }

        public void Run() {
            long r = 0L;
            for (int i = 0; i < int.MaxValue; i++) {
                r += _valueGetter.GetValue(i);
            }
        }
    }

    internal struct StructValueGetter : IValueGetter {
        public readonly int GetValue(int index) {
            return index + 3;
        }
    }

    internal struct StructValueGetter2(int someField) : IValueGetter {
        public readonly int GetValue(int index) {
            return index + 5;
        }
    }

    internal class ClassValueGetter1 : IValueGetter {
        public int GetValue(int index) {
            return index + 5;
        }
    }

    internal class ClassValueGetter2 : IValueGetter {
        public int GetValue(int index) {
            return index + 7;
        }
    }

    internal static class Demo2 {
        public static  void Run() {
            var t1 = new MyTestClass<StructValueGetter>(new StructValueGetter());
            RunDemo("StructValueGetter ", t1.Run);
            var t2 = new MyTestClass<ClassValueGetter1>(new ClassValueGetter1());
            RunDemo("ClassValueGetter1 ", t2.Run);
            var t3 = new MyTestClass<ClassValueGetter2>(new ClassValueGetter2());
            RunDemo("ClassValueGetter2 ", t3.Run);
            var t4 = new MyTestClass<IValueGetter>(new ClassValueGetter1());
            RunDemo("IValueGetter-1    ", t4.Run);


            var t5 = new MyTestClass<ClassValueGetter1>(new ClassValueGetter1());
            RunDemo("ClassValueGetter1 ", t5.Run);
            var t6 = new MyTestClass<StructValueGetter2>(new StructValueGetter2());
            RunDemo("StructValueGetter2", t6.Run);
            var t7 = new MyTestClass<IValueGetter>(new ClassValueGetter2());
            RunDemo("IValueGetter-2    ", t7.Run);
            var t8 = new MyTestClass<IValueGetter>(new StructValueGetter());
            RunDemo("IValueGetter-3    ", t8.Run);

            var t9 = Activator.CreateInstance(typeof(MyTestClass<>).MakeGenericType(typeof(StructValueGetter)), new StructValueGetter());
            Action action9 = (Action)Delegate.CreateDelegate(typeof(Action), t9, t9.GetType().GetMethod("Run"));
            RunDemo("Dynamic-Struct    ", action9);

        }

        static void RunDemo(string caption, Action action) {
            var stopWatch = Stopwatch.StartNew();
            action();
            stopWatch.Stop();
            Console.WriteLine($"{caption} time = {stopWatch.Elapsed}");
        }
    }
}

Demo2.Run();

在.net 8.0 Release 編譯執行的參考結果如下:

StructValueGetter  time = 00:00:00.6920186
ClassValueGetter1  time = 00:00:01.1887137
ClassValueGetter2  time = 00:00:05.2889692
IValueGetter-1     time = 00:00:01.1652195
ClassValueGetter1  time = 00:00:01.1625259
StructValueGetter2 time = 00:00:00.6488674
IValueGetter-2     time = 00:00:05.2114724
IValueGetter-3     time = 00:00:07.1394676
Dynamic-Struct     time = 00:00:00.6491220

結論

泛型編譯特性對效能有所影響,我們發現:

  • 泛型引數是 Struct 比 class 的效能要好,大約有兩倍的差異;
  • 泛型引數如果存在多個 Struct 可能時,效能沒有影響,但如果泛型引數存在多個 class 可能時,效能急劇下降5倍之多;
  • 泛型引數如果是介面形式,無論實際填充的結構還是類,其最終的執行效能一定是很慢的;
  • 使用反射(例如:MakeGenericType)構建出的泛型範例,其實際執行效能並不受影響,非常適合高度客製化的執行時型別構建,這一點非常重要,例如你可以在執行時檢測實際情況,構建出不同的比較器物件,雖然構建的工廠方法返回的是介面,但你可以使用反射的方式動態傳入字典的比較器引數(實際上c#的 Dictionary<TKey, TValue> 這點設計是失敗的,他的comparer不是一個泛型引數,而是介面);

綜上所述,瞭解C#泛型編譯特性對效能的影響是編寫高效能程式碼的重要一部分,合理使用對於關鍵性程式碼效能至關重要。