聊聊“装箱”在CLR内部的实现
原文连接: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?
聊聊“装箱”在CLR内部的实现的更多相关文章
- CLRMonitor - 跟踪CLR内部执行过程工具
CLRMonitor v1.0.1511.13 点击此处下载 软件介绍:这款软件主要用于跟踪CLR内部执行过程,定位当前程序执行的命名空间以及方法名等信息.可以迅速找到被跟踪程序的当前执行方法名.本软 ...
- CLR内部异常(下)
直接使用SEH 有些情况里直接使用SEH会更合适一些.特别是,如果需要在第一次遍历(first pass - SEH异常处理流程里的第一遍处理)时需要执行某些操作时,也就是在堆栈向上展开之前,SEH是 ...
- CLR内部异常(上)
当我们提到CLR里的“异常”,要注意一个很重要的区别.有通过如C#的try/catch/finally暴露给应用程序,并由运行时提供机制全权实现的托管异常.也有运行时自己使用的异常.大部分运行时开发人 ...
- CLR内部异常(中)
不捕捉某一个异常 常常有这种情况,代码不需要捕捉异常,但需要执行一些清理或者修正操作.虽然不总是,支持物(holders)经常用在这种场景里.在支持物(holders)不适用的情况里,CLR提供了两个 ...
- 教你配置windows上的windbg,linux上的lldb,打入clr内部这一篇就够了
一:背景 1. 讲故事 前几天公众号里有位兄弟看了几篇文章之后,也准备用windbg试试看,结果这一配就花了好几天,(づ╥﹏╥)づ,我想也有很多跃跃欲试的朋友在配置的时候肯定会遇到这样和那样的问题,所 ...
- [CLR via C#]5.3 值类型的装箱和拆箱
原文:[CLR via C#]5.3 值类型的装箱和拆箱 在CLR中为了将一个值类型转换成一个引用类型,要使用一个名为装箱的机制. 下面总结了对值类型的一个实例进行装箱操作时内部发生的事: 1)在托管 ...
- [CLR via C#]值类型的装箱和拆箱
我们先来看一个示例代码: namespace ConsoleApplication1 { class Program { static void Main(string[] args) { Array ...
- 深入探索.NET框架内部了解CLR如何创建运行时对象
原文地址:http://msdn.microsoft.com/en-us/magazine/cc163791.aspx 原文发布日期: 9/19/2005 原文已经被 Microsoft 删除了,收集 ...
- 深入探索.NET内部了解CLR如何创建运行时对象
前言 SystemDomain, SharedDomain, and DefaultDomain. 对象布局和内存细节. 方法表布局. 方法分派(Method dispatching). 因为公共语言 ...
随机推荐
- $Poj1723/AcWing123\ Soldiers$ 排序
$Poj$ $AcWing$ $Description$ $Sol$ 分别处理$x$坐标和$y$坐标.$y$坐标显然很好处理,就是排个序然后取中位数就好了.$x$没有$y$那么直接叭.所以我首先写了个 ...
- JVM性能优化系列-(1) Java内存区域
1. Java内存区域 1.1 运行时数据区 Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域.主要包括:程序计数器.虚拟机栈.本地方法栈.Java堆.方法区(运 ...
- 洛谷P1002 过河卒 题解 动态规划
题目链接:https://www.luogu.com.cn/problem/P1002 题目大意 棋盘上\(A\)点有一个过河卒,需要走到目标\(B\)点.卒行走的规则:可以向下.或者向右.同时在棋盘 ...
- 「CH2501」 矩阵距离 解题报告
CH2501 矩阵距离 描述 给定一个N行M列的01矩阵 A,\(A[i][j]\) 与 \(A[k][l]\) 之间的曼哈顿距离定义为: \(dist(A[i][j],A[k][l])=|i-k|+ ...
- c语言中自定义bool类型模板
首先,c语言中没有bool类型,只有c++中有,所以需要自定义,即c中表示bool类型的方法: 1.模板1 typedef int bool; #define false 0 #define true ...
- PHP 对接第三方 LINE 登录,网上找到相关的不多 但是网上哪些乱七八糟的啰啰嗦嗦 要么就是怎么做的, 什么步骤 总会给你省略, 如果有幸你看到我的 可以放心的复制即用, 当然 你也可以用postman去尝试 不过我觉得既然做开发 就没必要那个了! 如果用postman再最后一步的时候 请用本文最下方式
* LINE 官方文档:https://developers.line.biz/en/docs/line-login/getting-started/* 开发者平台地址:https://develop ...
- 31.用python中的serial向串口发送和接收数据(案例一)
代码功能说明:1.向串口助手发送十六进制数据:0X01,0X03,0X00,0X00,0X00,0X01,0X84,0X0A: 2.用串口助手向代码发送数据,并将发送过来的数据保存在数据库中,按数据和 ...
- Linux学习_菜鸟教程_1
Linux系统启动过程:内核的引导 .运行init.系统初始化.建立终端.用户登录系统 内核引导:计算机开机,然后BIOS开机自检,按照BIOS中设置的启动设备(通常是硬盘)来启动. 操作系统接管硬件 ...
- selenium中的xpath用法,使用xpath定位元素
xpath路径选择器定位元素 1. xpath: 使用路径表达式来定位xml或者html中文档中选取节点.在 XPath 中,有七种类型的节点:元素.属性.文本.命名空间.处理指令.注释以及文档节点( ...
- python方法的重写
方法的重写: 在子类中重写定义一个父类拥有的方法, 调用时使用子类中重写定义的方法. 效果图: 代码: class Animal: def run(self): print('动物会跑~~~') de ...