.NET 零開銷抽象指南

背景2008 年前后的 Midori 項目試圖構建一個以 .NET 為用戶態基礎的操作系統,在這個項目中有很多讓 CLR 以及 C# 的類型系統向著適合系統編程的方向改進的探索,雖然項目最終沒有面世,但是積累了很多的成果 。近些年由于 .NET 團隊在高性能和零開銷設施上的需要,從 2017 年開始 , 這些成果逐漸被加入 CLR 和 C# 中,從而能夠讓 .NET 團隊將原先大量的 C++ 基礎庫函數用 C# 重寫,不僅能減少互操作的開銷,還允許 JIT 進行 inline 等優化 。
與常識可能不同,將原先 C++ 的函數重寫成 C# 之后,帶來的結果反而是大幅提升了運行效率 。例如 Visual Studio 2019 的 16.5 版本將原先 C++ 實現的查找與替換功能用 C# 重寫之后 , 更是帶來了超過 10 倍的性能提升 , 在十萬多個文件中利用正則表達式查找字符串從原來的 4 分多鐘減少只需要 20 多秒 。
目前已經到了 .NET 7 和 C# 11,我們已經能找到大量的相關設施 , 不過我們仍處在改進進程的中途 。
本文則利用目前為止已有的設施,講講如何在 .NET 中進行零開銷的抽象 。
基礎設施首先我們來通過以下的不完全介紹來熟悉一下部分基礎設施 。
ref、out、inref readonly談到 refout,相信大多數人都不會陌生,畢竟這是從 C# 1 開始就存在的東西 。這其實就是內存安全的指針,允許我們在內存安全的前提之下,享受到指針的功能:
void Foo(ref int x){x++;}int x = 3;ref int y = ref x;y = 4;Console.WriteLine(x); // 4Foo(ref y);Console.WriteLine(x); // 5out 則多用于傳遞函數的結果,非常類似 C/C++ 以及 COM 中返回調用是否成功,而實際數據則通過參數里的指針傳出的方法:
bool TryGetValue(out int x){if (...){x = default;return false;}x = 42;return true;}if (TryGetValue(out int x)){Console.WriteLine(x);}in 則是在 C# 7 才引入的,相對于 ref 而言,in 提供了只讀引用的功能 。通過 in 傳入的參數會通過引用方式進行只讀傳遞,類似 C++ 中的 const T*
為了提升 in 的易用性,C# 為其加入了隱式引用傳遞的功能,即調用時不需要在調用處寫一個 in,編譯器會自動為你創建局部變量并傳遞對該變量的引用:
void Foo(in Mat3x3 mat){mat.X13 = 4.2f; // 錯誤,因為只讀引用不能修改}// 編譯后會自動創建一個局部變量保存這個 new 出來的 Mat3x3// 然后調用函數時會傳遞對該局部變量的引用Foo(new() {}); struct Mat3x3{public float X11, X12, X13, X21, X22, X23, X31, X32, X33;}當然,我們也可以像 ref 那樣使用 in,明確指出我們引用的是什么東西:
Mat3x3 x = ...;Foo(in x);struct 默認的參數傳遞行為是傳遞值的拷貝,當傳遞的對象較大時(一般指多于 4 個字段的對象),就會發生比較大的拷貝開銷,此時只需要利用只讀引用的方法傳遞參數即可避免,提升程序的性能 。
從 C# 7 開始 , 我們可以在方法中返回引用,例如:
ref int Foo(int[] array){return ref array[3];}調用該函數時 , 如果通過 ref 方式調用,則會接收到返回的引用:
int[] array = new[] { 1, 2, 3, 4, 5 };ref int x = ref Foo(array);Console.WriteLine(x); // 4x = 5;Console.WriteLine(array[3]); // 5否則表示接收值,與返回非引用沒有區別:
int[] array = new[] { 1, 2, 3, 4, 5 };int x = Foo(array);Console.WriteLine(x); // 4x = 5;Console.WriteLine(array[3]); // 4與 C/C++ 的指針不同的是,C# 中通過 ref 顯式標記一個東西是否是引用 , 如果沒有標記 ref,則一定不會是引用 。
當然 , 配套而來的便是返回只讀引用,確保返回的引用是不可修改的 。與 ref 一樣,ref readonly 也是可以作為變量來使用的:
ref readonly int Foo(int[] array){return ref array[3];}int[] array = new[] { 1, 2, 3, 4, 5 };ref readonly int x = ref Foo(array);x = 5; // 錯誤ref readonly int y = ref array[1];y = 3; // 錯誤ref structC# 7.2 引入了一種新的類型:ref struct 。這種類型由編譯器和運行時同時確保絕對不會被裝箱,因此這種類型的實例的生命周期非常明確,它只可能在棧內存中,而不可能出現在堆內存中:
Foo[] foos = new Foo[] { new(), new() }; // 錯誤ref struct Foo{public int X;public int Y;}借助 ref struct

推薦閱讀