原文連接:https://mattwarren.org/2017/08/02/A-look-at-the-internals-of-boxing-in-the-CLR/ 作者 Matt Warren。授權翻譯,轉載請保留原文鏈接。
它是.NET的基本組成部分,並且經常會在你不知情的情況下發生,但是它實際上是如何工作的呢?.NET運行時做了什么才使得裝箱成為可能?
注意:本文不會討論如何檢測裝箱,以及它是如何影響性能的或者如何避免裝箱發生(和Ben Adams來討論這些吧!)。本文只談論裝箱是如何工作的。
順便說一句,如果你喜歡讀一些關於CLR內部實現的內容,你會發現下面的文章會很有趣:
- How the .NET Runtime loads a Type
- Arrays and the CLR - a Very Special Relationship
- The CLR Thread Pool ‘Thread Injection’ Algorithm
- The 68 things the CLR does before executing a single line of your code
- How do .NET delegates work?
- Why is reflection slow?
- How does the ‘fixed’ keyword work?
CLR規范中的裝箱
首先值得指出的是,裝箱是CLR規范“ECMA-335”的要求,因此運行時必須提供:

這意味着CLR需要處理一些關鍵事項,我們將在本文的后續部分中進行探討。
創建“裝箱”類型
運行時首先需要為每一個它加載的struct創建一個對應的引用類型(“裝箱類型”)。
你可以在運行時創建“方法表”的方法中找到一個實際的案例,在該方法中,運行時首先會檢查它是否在處理“值類型”,然后進行相應的操作。因此,任何struct的“裝箱類型”都是在導入.dll時預先創建的,之后它們可以在程序執行期間被用於“裝箱”操作。
上文引用的代碼中的注釋非常有趣,因為它揭示了運行時必須處理的一些底層細節:
// 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源代碼中的相關代碼行:
- 特定CPU的優化版本(會在運行時進行wired-up)
- JIT_BoxFastMP_InlineGetThread (AMD64 - multi-proc or Server GC, implicit TLS)
- JIT_BoxFastMP (AMD64 - multi-proc or Server GC)
- JIT_BoxFastUP (AMD64 - single-proc and Workstation GC)
- JIT_TrialAlloc::GenBox(..) (x86), which is independently wired-up
- 在常見情況下,JIT內聯“helper“函數調用,請參見Compiler :: impImportAndPushBox(..)
- 最通用的則是用作后備的未優化版本,MethodTable::Box(..)
- 最終會調用CopyValueClassUnchecked(..)
- 和Stack Overflow上的問題“Why is struct better with being less than 16 bytes?“的答案相關。
有趣的是,唯一得到這種“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 |
圖的關鍵點
- 原始的struct,在棧上。
- struct被裝箱到一個存在在堆上的object。
- 調整this指針,以使MyStruct::ToString()能夠正常工作。
(如果你想了解更多.NET object的內部機制,可以查看這篇有用的文章)
我們可以在下面的代碼鏈接中看到這一點,請注意,存根僅由一些匯編指令組成(它不如方法調用那么繁重),並且有特定於CPU的版本:
- MethodDesc::DoPrestub(..) (calls
MakeUnboxingStubWorker(..)
) - MakeUnboxingStubWorker(..) (calls
EmitUnboxMethodStub(..)
to create the stub)
運行時/JIT必須采取這些技巧來幫助維持這樣一種錯覺,即struct可以像class一樣運行,即使它們在底層區別很大。 請參閱Eric Lipperts對 How do ValueTypes derive from Object (ReferenceType) and still be ValueTypes? 問題的回答, 以對此有更多的了解。
希望這篇文章能讓你對“裝箱”的底層實現有所了解。
進一步閱讀
Useful code comments related to boxing/unboxing stubs
- MethodTableBuilder::AllocAndInitMethodDescChunk(..)
- MethodDesc::FindOrCreateAssociatedMethodDesc(..) (in genmeth.cpp)
- Compiler::impImportBlockCode(..)
- Note on different ‘Boxing’ modes, added as part of the work on JIT: modify box/unbox/isinst/castclass expansions for fast jitting
GitHub Issues
- Question: Boxing on stack for function calls
- Boxing Cache?
- Improve the default hash code for structs (read the whole discussion)
- JIT: Fix value type box optimization
- (Discussion) Lightweight Boxing?
Other similar/related articles
- .NET Type Internals - From a Microsoft CLR Perspective (section on ‘Boxing and Unboxing’)
- C# value type boxing under the hood (section on ‘Interface call into the value type instance method’)
- Value type methods – call, callvirt, constrained and hidden boxing
- Performance Quiz #12 – The Cost of a Good Hash – Solution (Rico Mariani)
- To box or not to box (Eric Lippert)
- Beware of implicit boxing of value types
- Method calls on value types and boxing
Stack Overflow Questions
- CLR specification on boxing
- How CLR works when invoking a method of a struct
- boxing on structs when calling ToString()
- Does calling a method on a value type result in boxing in .NET?
- Why does implicitly calling toString on a value type cause a box instruction
- Why is struct better with being less than 16 bytes
- When are Type Objects for Value Types created?
- If my struct implements IDisposable will it be boxed when used in a using statement?
- When does a using-statement box its argument, when it’s a struct?
