在.NET最初被設計出來時,方法在默認情況下必須是非虛方法。這有幾個原因,其中一個是,非虛方法通常比虛方法快很多。除了虛函數表查詢本身的成本之外,虛函數通常還無法內聯。由于.NET的發展趨勢是傾向于使用大量的小方法,所以非內聯方法的函數調用開銷最終會超過方法本身的開銷。我們在文章“關于C#的抽象與For-Each性能”中介紹了這種內聯的部分效果。
在過去的幾年中,我們習慣的C#一直在變化。以前,大接口并不常見,但現在,可以完全匹配所有類的“影子接口”都非常常見了。這始于WCF,它鼓勵這種做法,雖然不是必須的。隨著DI框架性能的提升,在項目中見到面向所有非DTO類的影子接口已經很平常了。
方法去虛有多種方式,本質上講,就是在特定的情況下把它們視為非虛方法。Java HotSpot就以具備這項特性而聞名。在Java中,所有方法在默認情況下都是虛方法,因此,在Java的歷史中,解決這種性能問題的需求出現得早很多。
在今年三月份,.NET Core悄悄地對“去虛(Devirtualization)”發起了挑戰。簡單去虛特性處理了三種基本的場景:
在sealed類上調用虛方法; 在sealed方法上調用虛方法; 在明確知道類型的情況下調用虛方法(例如,緊挨著構造函數)。接口去虛也有一些基礎的支持,但有限制。例如:
如果方法是final的,而類不明確或者不是final的,則不允許接口去虛,因為派生類在實現接口時還可以重寫final方法。
需要注意的是,僅僅將類標記為“sealed”是不足以從去虛接口受益的。如果你正在使用DI框架隱藏運行時使用了哪個具體類,那么JIT編譯器可能無法確定使用了什么類型。
這在Java中之所以不是問題,是因為Java去虛技術的工作原理完全不同。它是根據運行時指標試探性地去虛接口調用,對最常調用的方法重新即時編譯。其中還包含了專門的防衛語句,以防具體的類型修改和去虛需要解除。
展望
.NET Core 2.0提供了上述特性,但還有許多工作要做。下面是去虛路線圖上的部分重點工作。
眾所周知,在涉及接口調用時,結構很糟糕,因為它們不僅是虛的,而且還需要對值進行裝箱。因此,有幾項工作是為了盡可能地減少虛調用和裝箱。其中一個重要的部分是一類結構,這是一個高級JIT概念,超出了本報道的范圍。
JIT本身的類型跟蹤改進。顯然,在許多情況下,JIT在一個地方知道具體的類型,但無法將信息傳遞下去,因此,JIT不得不采用更通用的機器代碼。
試探性去虛也在考慮范圍。根據概述,該特性不會和Java里的一樣。更準確地說,它會根據JIT過程中已知的覆寫清單做決定。(據推測,這會用在接口只有單個類實現的情況下,這在上文提到的DI場景下經常發生。)
其中有一種特殊情況,就是EqualityComparer.Default防衛去虛。由于在絕大多數情況下,IEqualityComparer調用都會指向默認實現(視情況不同,要么是IEquatable,要么是Object.Equals),所以他們覺得,如果不減慢使用非默認IEqualityComparer的情況,那么提升使用默認實現場景的執行速度是值得的。
查看英文原文:Devirtualization in .NET Core