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#泛型編譯特性對性能的影響是編寫高性能代碼的重要一部分,合理使用對於關鍵性代碼性能至關重要。