原文连接:https://mattwarren.org/2017/08/02/A-look-at-the-internals-of-boxing-in-the-CLR/ 作者 Matt Warren。授权翻译,转载请保留原文链接。

它是.NET的基本组成部分,并且经常会在你不知情的情况下发生,但是它实际上是如何工作的呢?.NET运行时做了什么才使得装箱成为可能?

注意:本文不会讨论如何检测装箱,以及它是如何影响性能的或者如何避免装箱发生(和Ben Adams来讨论这些吧!)。本文只谈论装箱是如何工作的。


顺便说一句,如果你喜欢读一些关于CLR内部实现的内容,你会发现下面的文章会很有趣:


CLR规范中的装箱

首先值得指出的是,装箱是CLR规范“ECMA-335”的要求,因此运行时必须提供:

这意味着CLR需要处理一些关键事项,我们将在本文的后续部分中进行探讨。


创建“装箱”类型

运行时首先需要为每一个它加载的struct创建一个对应的引用类型(“装箱类型”)。

你可以在运行时创建“方法表”的方法中找到一个实际的案例,在该方法中,运行时首先会检查它是否在处理“值类型”,然后进行相应的操作。因此,任何struct的“装箱类型”都是在导入.dll时预先创建的,之后它们可以在程序执行期间被用于“装箱”操作。

上文引用的代码中的注释非常有趣,因为它揭示了运行时必须处理的一些底层细节:

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

特定CPU的代码生成

但是,为了了解程序执行期间会发生什么,让我们从一个简单的C#程序开始。 下面的代码创建了一个自定义的struct或者说值类型,然后对其“装箱”和“拆箱”:

  1. public struct MyStruct
  2. {
  3. public int Value;
  4. }
  5. var myStruct = new MyStruct();
  6. // boxing
  7. var boxed = (object)myStruct;
  8. // unboxing
  9. var unboxed = (MyStruct)boxed;

以上的C#代码将变成以下IL代码,在其中你可以看到box和unbox.any 这2个IL指令:

  1. L_0000: ldloca.s myStruct
  2. L_0002: initobj TestNamespace.MyStruct
  3. L_0008: ldloc.0
  4. L_0009: box TestNamespace.MyStruct
  5. L_000e: stloc.1
  6. L_000f: ldloc.1
  7. 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的值:

  1. public struct MyStruct
  2. {
  3. public int Value;
  4. public override string ToString()
  5. {
  6. return "Value = " + Value.ToString();
  7. }
  8. }

现在,如果我们查看运行时为装箱版本的MyStruct创建的“方法表”(请记住,值类型没有“方法表”),我们会发现发生了一些奇怪的事情。 请注意,MyStruct::ToString有2个条目,我将其中之一标记为“拆箱存根”

  1. Method table summary for 'MyStruct':
  2. Number of static fields: 0
  3. Number of instance fields: 1
  4. Number of static obj ref fields: 0
  5. Number of static boxed fields: 0
  6. Number of declared fields: 1
  7. Number of declared methods: 1
  8. Number of declared non-abstract methods: 1
  9. Vtable (with interface dupes) for 'MyStruct':
  10. Total duplicate slots = 0
  11. SD: MT::MethodIterator created for MyStruct (TestNamespace.MyStruct).
  12. slot 0: MyStruct::ToString 0x000007FE41170C10 (slot = 0) (Unboxing Stub)
  13. slot 1: System.ValueType::Equals 0x000007FEC1194078 (slot = 1)
  14. slot 2: System.ValueType::GetHashCode 0x000007FEC1194080 (slot = 2)
  15. slot 3: System.Object::Finalize 0x000007FEC14A30E0 (slot = 3)
  16. slot 5: MyStruct::ToString 0x000007FE41170C18 (slot = 4)
  17. <-- vtable ends here

完整版戳

那么,这个“拆箱存根”是什么?为什么需要?

之所以需要它,是因为如果你在装箱版的MyStruct上调用ToString()方法,会调用在MyStruct内声明的重写方法(这是你想要执行的操作),而不是Object::ToString()的版本。 但是,MyStruct::ToString()希望能够访问struct中的任何字段,例如本例中的Value。 为此,运行时/JIT必须在调用MyStruct::ToString()之前调整this指针,如下图所示:

  1. 1. MyStruct: [0x05 0x00 0x00 0x00]
  2. | Object Header | MethodTable | MyStruct |
  3. 2. MyStruct (Boxed): [0x40 0x5b 0x6f 0x6f 0xfe 0x7 0x0 0x0 0x5 0x0 0x0 0x0]
  4. ^
  5. object 'this' pointer |
  6. | Object Header | MethodTable | MyStruct |
  7. 3. MyStruct (Boxed): [0x40 0x5b 0x6f 0x6f 0xfe 0x7 0x0 0x0 0x5 0x0 0x0 0x0]
  8. ^
  9. 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
 

聊聊“装箱”在CLR内部的实现的更多相关文章

  1. CLRMonitor - 跟踪CLR内部执行过程工具

    CLRMonitor v1.0.1511.13 点击此处下载 软件介绍:这款软件主要用于跟踪CLR内部执行过程,定位当前程序执行的命名空间以及方法名等信息.可以迅速找到被跟踪程序的当前执行方法名.本软 ...

  2. CLR内部异常(下)

    直接使用SEH 有些情况里直接使用SEH会更合适一些.特别是,如果需要在第一次遍历(first pass - SEH异常处理流程里的第一遍处理)时需要执行某些操作时,也就是在堆栈向上展开之前,SEH是 ...

  3. CLR内部异常(上)

    当我们提到CLR里的“异常”,要注意一个很重要的区别.有通过如C#的try/catch/finally暴露给应用程序,并由运行时提供机制全权实现的托管异常.也有运行时自己使用的异常.大部分运行时开发人 ...

  4. CLR内部异常(中)

    不捕捉某一个异常 常常有这种情况,代码不需要捕捉异常,但需要执行一些清理或者修正操作.虽然不总是,支持物(holders)经常用在这种场景里.在支持物(holders)不适用的情况里,CLR提供了两个 ...

  5. 教你配置windows上的windbg,linux上的lldb,打入clr内部这一篇就够了

    一:背景 1. 讲故事 前几天公众号里有位兄弟看了几篇文章之后,也准备用windbg试试看,结果这一配就花了好几天,(づ╥﹏╥)づ,我想也有很多跃跃欲试的朋友在配置的时候肯定会遇到这样和那样的问题,所 ...

  6. [CLR via C#]5.3 值类型的装箱和拆箱

    原文:[CLR via C#]5.3 值类型的装箱和拆箱 在CLR中为了将一个值类型转换成一个引用类型,要使用一个名为装箱的机制. 下面总结了对值类型的一个实例进行装箱操作时内部发生的事: 1)在托管 ...

  7. [CLR via C#]值类型的装箱和拆箱

    我们先来看一个示例代码: namespace ConsoleApplication1 { class Program { static void Main(string[] args) { Array ...

  8. 深入探索.NET框架内部了解CLR如何创建运行时对象

    原文地址:http://msdn.microsoft.com/en-us/magazine/cc163791.aspx 原文发布日期: 9/19/2005 原文已经被 Microsoft 删除了,收集 ...

  9. 深入探索.NET内部了解CLR如何创建运行时对象

    前言 SystemDomain, SharedDomain, and DefaultDomain. 对象布局和内存细节. 方法表布局. 方法分派(Method dispatching). 因为公共语言 ...

随机推荐

  1. FPGA之乒乓操作

    1.乒乓操作原理 乒乓操作是一个主要用于数据流控制的处理技巧,典型的乒乓操作如图所示: 外部输入数据流通过“输入数据选择控制”模块送入两个数据缓冲区中,数据缓冲模块可以为任何存储模块,比较常用的存储单 ...

  2. A记录都不懂,怎么做开发Leader?

    开发 Leader 和一线开发的区别在于:普通一线开发很多时候都只接触业务编码,不需要关注除开发之外的其他事情.但是作为一个开发 Leader,不仅仅需要懂开发层面的东西,还需要懂得运维层面的东西. ...

  3. OAuth2.0概念以及实现思路简介

    一.什么是OAuth? OAuth是一个授权规范,可以使A应用在受限的情况下访问B应用中用户的资源(前提是经过了该用户的授权,而A应用并不需要也无法知道用户在B应用中的账号和密码),资源通常以REST ...

  4. Vuex入门实践(中)-多module中的state、mutations、actions和getters

    一.前言 上一篇文章<Vuex入门实践(上)>,我们一共实践了vuex的这些内容: 1.在state中定义共享属性,在组件中可使用[$store.state.属性名]访问共享属性 2.在m ...

  5. 830. String Sort

    830. String Sort 题解 int alpha[256] = {0};//记录字符的次数 bool cmp(char a,char b) { if(alpha[a]==alpha[b])/ ...

  6. linux下安装cmake方法(1)---下载压缩包

    OpenCV 2.2以后的版本需要使用Cmake生成makefile文件,因此需要先安装cmake:还有其它一些软件都需要先安装cmake 1.在linux环境下打开网页浏览器,输入网址:http:/ ...

  7. Vim的环境设定与记录

    vim 会主动将曾经做过的行为记录下来,记录在文件   ~/.viminfo,好方便下次作业. 更改  /etc/vimrc配置操作环境 vim的环境设定参数 :set nu :set  nonu 设 ...

  8. redux一些自习时候自己写的的单词

    setState:设置状态 render:渲染,挂载 dispatchEvent : 派发事件 dispatch:分发,派遣:库里的一个方法,简而言之相当于一个actions和reducer监听方法更 ...

  9. 解决a标签点击会出现虚框现象

    1.解决a标签点击会出现虚框现象. 当a标签获得焦点的时候,a标签的周围就会出现虚框,它不同于border,不占任何宽度,a失去焦点的时候就会消失,就是outline. 在遨游,Firefox ,IE ...

  10. Trailhead Lightning 学习 一

    计划学习一下莱特宁,从最基本的开始学习,脚踏实地.不忘初心,牢记使命,以下是查阅的资料. 简介 在此Salesforce教程中,将阐述Salesforce Lightning的基础知识,并了解Sale ...