C# 7.2 通過 in 和 readonly struct 減少方法值複製提高效能
在 C# 7.2 提供了一系列的方法用於方法引數傳輸的時候減少對結構體的複製從而可以高效使用記憶體同時提高效能
在開始閱讀之前,希望讀者對 C# 的值型別、引用型別有比較深刻的認知。
在 C# 中,如果對記憶體有嚴格的要求,同時需要減少 GC 的情況,推薦此時使用結構體。但是結構體有一個缺點在於,結構體在每次呼叫方法作為引數傳遞的時候都會新建一個副本,這對於效能要求特別高的情況是不適合的。
定義一個值型別
struct Int256 { public Int256(long bits0, long bits1, long bits2, long bits3) { Bits0 = bits0; Bits1 = bits1; Bits2 = bits2; Bits3 = bits3; } public long Bits0 { set; get; } public long Bits1 { get; } public long Bits2 { get; } public long Bits3 { get; } }
此時通過一個簡單的賦值就可以獲取複製
Int256 f1 = new Int256(0, 1, 2, 1); var f2 = f1; f2.Bits0 = 2; Console.WriteLine($"f1.bits0={f1.Bits0} f2.bits0={f2.Bits0}"); //f1.bits0=0 f2.bits0=2
在呼叫方法的時候也一樣,傳入引數就是複製一個新的值
static void Main(string[] args) { Int256 f1 = new Int256(0, 1, 2, 1); Foo(f1); Console.WriteLine($"{f1.Bits0}"); // 0 } private static void Foo(Int256 foo) { foo.Bits0 = 2; }
對於很小的值型別,如果小於 IntPtr.Size 的傳輸,會比引用傳遞的複製速度快,但是對比比較大的值型別,如上面定義的,複製一次需要的時間會比較長
特別是存在很多次的值傳遞的時候,如下面的程式碼,會呼叫 1000 次的值傳遞。除了效能的問題,還存在堆疊的記憶體的問題
定義一個很大的值型別,裡面包含 10000 個 double 看起來就很大
struct Double10000 { public double Double0 { get; } public double Double1 { get; } public double Double2 { get; } …… public double Double9999 { get; } }
用遞迴的方式進行呼叫,執行的時候很快就可以看到堆疊都被申請的值傳遞使用,同時 CPU 的使用很高
static void Main(string[] args) { Double10000 foo = new Double10000(); Foo(foo); } private static void Foo(Double10000 foo, int n = 100) { if (n == foo.Double0) { return; } Foo(foo, n - 1); }
如果可以讓值型別和引用一樣傳遞,是不是就可以減少值型別的複製同時減少堆疊的使用,請注意不要糾結值型別是分配在堆中還是棧中的問題,上面的程式碼更多的是方法的遞迴
對比記憶體的使用,更多的時候關心的是執行的速度。新增一些程式碼用來測試效能,同時減少呼叫
var st = new Stopwatch(); st.Start(); Foo(foo); st.Stop(); Console.WriteLine(st.ElapsedTicks);
private static void Foo(Double10000 foo, int n = 10) { if (n == foo.Double0) { return; } Foo(foo, n - 1); }
這裡輸出的 ElapsedTicks 的單位是 100ns 需要知道 1ms=1000000ns
也就是 1w 的 tick 就是 1 毫秒,下面我執行 3 次程式碼,收集到的值
在 C# 7.2 可以使用 in 關鍵字告訴 VisualStudio 當前的方法不會對傳進來的結構體進行修改,當前這樣寫只是語法層面。如果有一些厲害的黑客,可能還繼續這樣寫入,於是為了防止真的進行修改,在底層還是複製了一份。
也就是隻是在引數裡面使用了 in 是不夠的,具體請看 ofollow,noindex" target="_blank">這個拖後腿的“in” - Bean.Hsiang - 部落格園
如果想要更好的使用記憶體同時提高效能,只有在可以被標記為只讀的結構體的時候使用 in 才可以
先將 Double100 標記為 readonly 如果一個值型別標記為 readonly 也就無法對裡面的欄位或屬性進行設定了
在 Foo 傳入的方法引數標記 in 這樣就完成了,因為 in 表示對引數不進行修改,而傳入的是 readonly struct 本來就不能被修改,於是就傳入 struct 的引用
readonly struct Double10000 private static void Foo(in Double10000 foo, int n = 10) { if (n == foo.Double0) { return; } Foo(foo, n - 1); }
同樣執行 3 次,可以看到速度是原來的 10 倍
同時佔用的堆疊更小,可以使用更多的遞迴,修改 Foo 函式呼叫次數為 1000 可以看到還能執行,但是如果去掉了引數 in 最多隻能呼叫 20 次
沒有加 in 的引數,運行了 17 次
添加了 in 之後因為不需要複製值,速度和使用記憶體都比較好