聊聊「裝箱」在CLR內部的實現

原文鏈接:https://mattwarren.org/2017/08/02/A-look-at-the-internals-of-boxing-in-the-CLR/ 做者 Matt Warren。受權翻譯,轉載請保留原文連接。

它是.NET的基本組成部分,而且常常會在你不知情的狀況下發生,可是它其實是如何工做的呢?.NET運行時作了什麼才使得裝箱成爲可能?html

注意:本文不會討論如何檢測裝箱,以及它是如何影響性能的或者如何避免裝箱發生(和Ben Adams來討論這些吧!)。本文只談論裝箱是如何工做的。node


順便說一句,若是你喜歡讀一些關於CLR內部實現的內容,你會發現下面的文章會頗有趣:git


CLR規範中的裝箱

首先值得指出的是,裝箱是CLR規範「ECMA-335」的要求,所以運行時必須提供:github

這意味着CLR須要處理一些關鍵事項,咱們將在本文的後續部分中進行探討。編程


建立「裝箱」類型

運行時首先須要爲每個它加載的struct建立一個對應的引用類型(「裝箱類型」)。less

你能夠在運行時建立「方法表」的方法中找到一個實際的案例,在該方法中,運行時首先會檢查它是否在處理「值類型」,而後進行相應的操做。所以,任何struct的「裝箱類型」都是在導入.dll時預先建立的,以後它們能夠在程序執行期間被用於「裝箱」操做。ide

上文引用的代碼中的註釋很是有趣,由於它揭示了運行時必須處理的一些底層細節:wordpress

// Check to see if the class is a valuetype; but we don't want to mark System.Enum
// as a ValueType. To accomplish this, the check takes advantage of the fact
// that System.ValueType and System.Enum are loaded one immediately after the
// other in that order, and so if the parent MethodTable is System.ValueType and
// the System.Enum MethodTable is unset, then we must be building System.Enum and
// so we don't mark it as a ValueType.

特定CPU的代碼生成

可是,爲了瞭解程序執行期間會發生什麼,讓咱們從一個簡單的C#程序開始。 下面的代碼建立了一個自定義的struct或者說值類型,而後對其「裝箱」和「拆箱」:函數

public struct MyStruct { public int Value; } var myStruct = new MyStruct(); // boxing var boxed = (object)myStruct; // unboxing var unboxed = (MyStruct)boxed; 

以上的C#代碼將變成如下IL代碼,在其中你能夠看到box和unbox.any 這2個IL指令:性能

L_0000: ldloca.s myStruct
L_0002: initobj TestNamespace.MyStruct
L_0008: ldloc.0 
L_0009: box TestNamespace.MyStruct
L_000e: stloc.1 
L_000f: ldloc.1 
L_0010: unbox.any TestNamespace.MyStruct

Runtime and JIT code

那麼,JIT如何處理這些IL操做碼呢? 一般狀況下,它會鏈接(wires up)內聯(inline)運行時提供的「JIT Helper 方法「——通過優化而且手寫的彙編代碼。 下面的連接會帶你進入CoreCLR源代碼中的相關代碼行:

有趣的是,惟一獲得這種「JIT Helper 方法「特殊待遇是object,string以及array的分配,這剛好說明了裝箱對性能的敏感性。

做爲對比,「拆箱「只有一個叫作JIT_Unbox(..)的」helper方法「,在一些不常見的狀況下有可能會使用JIT_Unbox_Helper(..)做爲後備方法。它的鏈接能夠查看這裏( CORINFO_HELP_UNBOX 到 JIT_Unbox )。在常見的狀況下,JIT也會將這個helper方法進行內聯以節約方法調用的開銷,詳情查看Compiler::impImportBlockCode(..)

請注意,「Unbox helper」僅獲取「裝箱」數據的引用/指針,而後必須將其放入堆棧中。 正如咱們在上面看到的,當C#編譯器執行拆箱操做時,它使用的是「Unbox_Any」操做碼,而不只是「Unbox」,請參見Unboxing does not create a copy of the value以獲取更多信息。(Unbox_Any等價於unbox操做以後再執行ldobj操做,即拷貝操做——譯者注)。


建立拆箱存根

除了對一個struct進行「裝箱」和「拆箱」外,運行時一樣須要在一個類型處於「裝箱」的時間內提供幫助。要了解這樣說的緣由,讓咱們來拓展MyStruct而且對ToString()方法進行重寫,以使得它顯示當前Value的值:

public struct MyStruct { public int Value; public override string ToString() { return "Value = " + Value.ToString(); } } 

如今,若是咱們查看運行時爲裝箱版本的MyStruct建立的「方法表」(請記住,值類型沒有「方法表」),咱們會發現發生了一些奇怪的事情。 請注意,MyStruct::ToString有2個條目,我將其中之一標記爲「拆箱存根」

Method table summary for 'MyStruct':
 Number of static fields: 0
 Number of instance fields: 1
 Number of static obj ref fields: 0
 Number of static boxed fields: 0
 Number of declared fields: 1
 Number of declared methods: 1
 Number of declared non-abstract methods: 1
 Vtable (with interface dupes) for 'MyStruct':
   Total duplicate slots = 0

 SD: MT::MethodIterator created for MyStruct (TestNamespace.MyStruct).
   slot  0: MyStruct::ToString  0x000007FE41170C10 (slot =  0) (Unboxing Stub)
   slot  1: System.ValueType::Equals  0x000007FEC1194078 (slot =  1) 
   slot  2: System.ValueType::GetHashCode  0x000007FEC1194080 (slot =  2) 
   slot  3: System.Object::Finalize  0x000007FEC14A30E0 (slot =  3) 
   slot  5: MyStruct::ToString  0x000007FE41170C18 (slot =  4) 
   <-- vtable ends here

完整版戳

那麼,這個「拆箱存根」是什麼?爲何須要?

之因此須要它,是由於若是你在裝箱版的MyStruct上調用ToString()方法,會調用在MyStruct內聲明的重寫方法(這是你想要執行的操做),而不是Object::ToString()的版本。 可是,MyStruct::ToString()但願可以訪問struct中的任何字段,例如本例中的Value。 爲此,運行時/JIT必須在調用MyStruct::ToString()以前調整this指針,以下圖所示:

1. MyStruct:         [0x05 0x00 0x00 0x00]

                     |   Object Header   |   MethodTable  |   MyStruct    |
2. MyStruct (Boxed): [0x40 0x5b 0x6f 0x6f 0xfe 0x7 0x0 0x0 0x5 0x0 0x0 0x0]
                                          ^
                    object 'this' pointer | 

                     |   Object Header   |   MethodTable  |   MyStruct    |
3. MyStruct (Boxed): [0x40 0x5b 0x6f 0x6f 0xfe 0x7 0x0 0x0 0x5 0x0 0x0 0x0]
                                                           ^
                                   adjusted 'this' pointer |

圖的關鍵點

  1. 原始的struct,在棧上。
  2. struct被裝箱到一個存在在堆上的object。
  3. 調整this指針,以使MyStruct::ToString()可以正常工做。

(若是你想了解更多.NET object的內部機制,能夠查看這篇有用的文章

咱們能夠在下面的代碼連接中看到這一點,請注意,存根由一些彙編指令組成(它不如方法調用那麼繁重),而且有特定於CPU的版本:

運行時/JIT必須採起這些技巧來幫助維持這樣一種錯覺,即struct能夠像class同樣運行,即便它們在底層區別很大。 請參閱Eric Lipperts對 How do ValueTypes derive from Object (ReferenceType) and still be ValueTypes? 問題的回答, 以對此有更多的瞭解。

 


 

但願這篇文章能讓你對「裝箱」的底層實現有所瞭解。

 


進一步閱讀

Useful code comments related to boxing/unboxing stubs

GitHub Issues

Other similar/related articles

Stack Overflow Questions

 

 

 
 
歡迎你們關注個人公衆號"慕容的遊戲編程":chenjd01
相關文章
相關標籤/搜索