草庐IT

【BotR】CLR类型系统

netry's blog 2023-03-28 原文

.NET运行时之书(Book of the Runtime,简称BotR)是一系列描述.NET运行时的文档,2007年左右在微软内部创建,最初目的是为了帮助其新员工快速上手.NET运行时;随着.NET开源,BotR也被公开了出来,如果想深入理解CLR,这系列文章不可错过。

BotR系列目录:
[1] CLR类型加载器设计(Type Loader Design)
[2] CLR类型系统概述(Type System Overview)

类型系统概述(Type System Overview)

原文:https://github.com/dotnet/runtime/blob/main/docs/design/coreclr/botr/type-system.md
作者: David Wrighton - 2010
翻译:几秋 (https://www.cnblogs.com/netry/)

介绍

CLR类型系统是我们在ECMA规范+扩展中描述的类型系统的表示形式。

概述

该类型系统是由一系列数据结构(其中一些在BotR的其它章节有描述)和操作这些数据结构的算法组合而成,它不是通过反射暴露出来的类型系统,尽管反射确实依赖于这个系统。

由类型系统维护的主要数据结构是:

  • MethodTable
  • EEClass
  • MethodDesc
  • FieldDesc
  • TypeDesc
  • ClassLoader

由类型系统包含的主要算法是:

  • Type Loader: 用于加载类型并创建类型系统中大部分的重要数据结构。
  • CanCastTo and similar: 类型比较功能。
  • LoadTypeHandle: 主要用于查找类型。
  • Signature parsing: 用于比较和收集有关方法和字段的信息
  • GetMethod/FieldDesc: 用于查找、加载方法和字段。
  • Virtual Stub Dispatch: 用于查找对接口的虚调用的存根。

还有很多辅助数据结构和算法为CLR的其余部分提供各种信息,但它们对于整个系统的理解并不那么重要。

组件架构

类型系统的数据结构通常被各种算法所使用。本文档不会涉及类型系统算法(因为BotR的其它章节有涉及这些),但是它会试图描述各种主要数据结构。

依赖

类型系统大体上是给CLR中的很多部分提供服务,多数核心组件都对类型系统的行为有某种形式的依赖性。下图描述了影响类型系统的通用数据流,它不是很全面,但是指出了只要的信息流。

组件依赖

类型系统的主要依赖关系如下:

  • 加载器工作需要获取正确的元数据。
  • 元数据系统(metadata system)提供一个元数据API去收集信息。
  • 安全系统(security system)告知类型系统是否允许某些类型系统结构。
  • 应用程序域(AppDomain)提供一个LoaderAllocator去处理类型系统数据结构的分配行为。

依赖于此组件的组件

类型系统有3个主要组件依赖于它:

  • jit接口和jit helpers主要依赖于类型、方法,和字段搜索功能。一旦找到了类型系统对象,返回的数据结构就会进行裁剪,以提供jit所需的信息。
  • 反射使用类型系统提供对ECMA标准化概念相对简单的访问,CLR类型系统的数据结构中正好有这些。
  • 常规托管代码执行 需要使用类型系统进行类型比较逻辑和虚拟存根分派

类型系统设计

核心类型系统数据结构是表示实际加载类型的数据结构(例如,TypeHandle, MethodTable, MethodDesc, TypeDesc, EEClass)和允许在加载类型后找到它们的数据结构(例如,ClassLoader, Assembly, Module, RIDMaps)。

加载类型的数据结构和算法在BotR的Type LoaderMethodDesc章节中有讨论。

将这些数据结构绑定在一起是一组功能, 允许JIT/Reflection/TypeLoader/stackwalker去查找现存类型和方法,一般的想法是,这些搜索应该很容易地由ECMA CLI规范中指定的元数据令牌/签名(metadata tokens/signatures)驱动。

最后,当合适的类型系统数据结构被找到,我们有算法从类型中收集信息,有and/or比较两个类型。可以在 Virtual Stub Dispatch找到这种算法的一个特别复杂的例子。

设计目标和非目标

目标

  • 访问运行时执行(非反射)代码所需的信息要非常快。
  • 访问编译时生成代码所需的信息要是非常直截了当的。
  • 垃圾回收器(garbage collector)、堆栈遍历器(stackwalker,没找到准确的中文翻译,详情可参考这篇文章 Stackwalking in the .NET Runtime/)无需锁或分配内存就可以方法信息。
  • 一次只加载最少量的类型。
  • 在类型加载时加载最小数量的给定类型。
  • 类型系统的数据结构必须存储在NGEN镜像中。

非目标

  • 元数据中的所有信息都直接反映在CLR的数据结构中。
  • 所有的反射操作都要非常快。

运行时在托管代码执行期间使用的一个典型算法设计

类型转换算法(casting algorithm)是类型系统中的典型算法,在托管代码的执行过程中大量使用这种算法。

这个算法至少有4个独立的入口(entry point),选择每个入口都是为了提供不同的快速路径,希望能够实现最佳的性能。

  • 一个对象能否被转换成一个非类型等价的非数组类型(non-type equivalent non-array type)?
  • 一个对象能否被转换成一个没有实现泛型变体(generic variance)的接口类型?
  • 一个对象能否被转换成一个数组类型?
  • 一个类型的对象能否被转换成转换为任意其他托管类型?

除了最后一个之外,每个实现都进行了优化,以便在不完全通用的情况下提高性能。例如,“一个类型能否被转换成一个父类?”就是 “一个对象能否被转换成一个非类型等价的非数组类型”的变体,它通过单循环遍历一个单链表实现。这只能搜索可能的转换操作的一个子集,但可以通过检查试图强制转换的类型来确定是否是合适的集合,这个算法在jit helper JIT_ChkCastClass_Portable中实现。

假设:

  • 算法的特殊用途实现通常是性能改进。
  • 额外版本的算法不会提供无法克服的维护问题(insurmountable maintenance problem)。

类型系统中典型搜索算法设计

在类型系统中很多算法遵循这种常见模式。类型系统通常用于查找类型,这可以通过任意数量的输入触发,如JIT、反射、序列化、远程调用等等。在这些情况下,对类型系统的基本输入是:

  • 来自开始搜索的上下文(一个模块或者程序集指针)。
  • 在初始上下文中描述所需类型的标识符(identifier),通常是一个令牌(token),或者一个字符串(如果搜索上下文是一个程序集)。

这个算法必须首先解码标识符。对于搜索一个类型的场景,令牌可能是typedef令牌、typeref令牌、typespec令牌,或者是一个字符串。这些不同种类的标识符都会将导致不同形式的查询。

  • 一个typedef令牌将导致在模块的RidMap中进行查找。这是一个简单的数组索引。
  • 一个typeref令牌将导致查找此令牌所引用的程序集,然后用找到的汇编指针和从typeref表中收集的字符串重新开始类型查找算法。
  • 一个typespec令牌指示必须对签名(signature)进行解析才能找到签名。解析签名以查找加载该类型所需的信息,这将递归的触发更多类型查找。
  • 名称(name)用于在程序集之间进行绑定。TypeDef/ExportedTypes表用来做匹配搜索。主要:此搜索通过清单模块对象(manifest module object)上的哈希表进行优化。

从这个设计中可以明显看出一些搜索算法在类型系统中的共同特点:

  • 搜索使用的输入是和元数据加密耦合的。特别是元数据令牌和字符串名称,它们检查被传来传去,还有,这些搜索是和模块绑在一起的,它们直接映射到 .dll和.exe文件。
  • 使用缓存的信息去提高性能。RidMap和哈希表是经过优化的数据结构,用于改进这些查找。
  • 这些算法通常根据其输入有3-4个不同的路径。

除了这个总体设计,在此基础上,还有一些额外的需求:

  • 假设 在GC停止时搜索已加载的类型是安全的。
  • 不变性 一个已经加载了的类型将总是可以被搜索到。
  • 问题搜索程序依赖于元数据读取。某些场景可能会导致性能不足。

此搜索算法是 JIT期间使用的典型程序。它具有许多共同的特征:

  • 它使用元数据
  • 它需要在许多地方查找数据
  • 在我们的数据结构中有相对少量的重复数据
  • 它通常不会深度递归,也没有循环

这使我们能够满足性能要求,以及使用基于IL的JIT所必需的特征。

类型系统对垃圾回收器的要求

垃圾回收器要有关类型实例分配在GC堆上的信息,这是通过一个指向类型系统数据结构(MethodTable)的指针来完成,该MethodTable位于每一个托管对象的开头。附着到这个MethodTable之上的,是一个描述类型实例GC布局的数据结构。该布局有两种形式(一般类型和对象数组是一种,值类型数组是另一种)。

  • 假设:类型系统的数据结构有一个生命周期,它超过了自身描述类型的托管对象的生命周期。
  • 要求: 垃圾回收器需要在运行时挂起时执行堆栈遍历器(stack walker),这将在下面讨论。

Stackwalker对类型系统的要求

堆栈遍历器/GC堆栈遍历器在两种情况下要求类型系统输入:

  • 用于查找值类型在堆栈上的大小
  • 用于查找GC根以在堆栈上的值类型中报告 (原文为For finding GC roots to report within valuetypes on the stack,有点拗口,其实这个GC根就是栈根Stack roots,需要报告给垃圾回收器,是因为垃圾回收器需要根据这些信息来确定对象是否为活动对象

由于各种原因,包括希望延迟加载类型,以及避免生成多个版本的代码(仅通过相关的gc信息不同),CLR当前需要遍历堆栈上的方法签名,这种需求很少得到满足,因为它要求在特定的时刻执行堆栈遍历,但是为了满足我们的现实目标,当遍历堆栈的时候,必须能够遍历签名。

堆栈遍历器以大约 3 种模式执行:

  • 因为安全或者异常处理的原因,要遍历当前线程的堆栈
  • 因为GC,要遍历使用线程的堆栈(所有线程都被EE挂起)
  • 使用分析工具(profiler)需要遍历指定的线程(指定线程被挂起)

在GC和分析工具遍历堆栈的情况,由于线程被挂起,分配内存或占用大多数锁是不安全的。这导致我们开发了一条通过类型系统的路径,可以依赖它来遵循上述要求。型系统实现此目标所需的规则是:

  • 如果一个方法已经被调用,那么被调用方法的所有值类型参数都将被加载到进程中的某个应用程序域中。
  • 从带有签名的程序集到实现该类型的程序集,引用它们的程序集必须在遍历签名之前被解析,这是遍历堆栈中必要的一部分。

这是通过一系列广泛而复杂的强制措施来实施的,包括类型加载器、NGEN镜像生成过程和JIT。

  • 问题: 堆栈遍历器对类型系统的要求是非常脆弱的。
  • 问题: 在类型系统上实现堆栈遍历器的要求,需要侵入类型系统中的每个搜索类型时可能接触到的函数
  • 问题: 执行的签名遍历是使用正常的签名遍历代码完成的。此代码设计是在遍历签名时加载类型,但在这种情况下,类型加载功能是在假设实际上不会触发任何类型加载的情况下使用的。
  • 问题: 堆栈遍历器不仅需要类型系统的支持,还需要程序集加载器的支持,要满足类型系统的需要,加载器还有很多问题。

类型系统与NGEN

类型系统数据结构是保存到NGEN镜像中的核心部分,不幸的是,这些数据结构逻辑内有指向其它NGEN镜像的指针。为了处理这种情况,类型系统数据结构实现了一个称为恢复(restoration)的概念。
在恢复期,当第一次需要类型系统数据结构时,该数据结构用正确的指针固定, 这与类型加载级别有关,请看前篇CLR类型加载器设计

还存在一个预恢复(pre-restored)数据结构的概念,这意味着数据结构在NGEN镜像加载时足够正确(在intra-module指针和预先加载类型修正之后),数据结构可以按原样使用。此优化要求将NGEN镜像“硬绑定”("hard bound")到其依赖程序集,详情请查看NGEN相关文档。

类型系统和域中性加载(Domain Neutral Loading)

类型系统是实现域中性加载的核心部分,它通过在AppDomain创建时启用LoaderOptimization选项暴露给用户。Mscorlib始终作为域中性加载,此功能的核心要求是类型系统数据结构不能要求指向特定域状态(domain specific state)的指针,这主要表现在围绕静态字段和类构造函数的需求中。特别是,由于这个原因,一个类的构造函数是否已经运行不是核心MethodTable数据结构的一部分。并且有一种机制来存储附加到DomainFile数据结构而不是MethodTable数据结构。

物理结构

类型系统的主要部分位于:

  • Class.cpp/inl/h – EEClass函数, 和BuildMethodTable
  • MethodTable.cpp/inl/h – 操作methodtable的函数
  • TypeDesc.cpp/inl/h – 检查TypeDesc的函数
  • MetaSig.cpp SigParser – 签名代码
  • FieldDesc /MethodDesc –检查这些数据结构的函数
  • Generics – 泛型特定逻辑
  • Array – 处理数组处理所需的特殊情况的代码
  • VirtualStubDispatch.cpp/h/inl – 虚拟存根分派(VSD)代码
  • VirtualCallStubCpu.hpp – 用于虚拟存根分派的处理器特定代码

主要入口函数是BuildMethodTable、 LoadTypeHandleThrowing、CanCastTo*、 GetMethodDescFromMemberDefOrRefOrSpecThrowing、 GetFieldDescFromMemberRefThrowing、 CompareSigs和 VirtualCallStubManager::ResolveWorkerStatic.

相关阅读

有关【BotR】CLR类型系统的更多相关文章

  1. ruby - Infinity 和 NaN 的类型是什么? - 2

    我可以得到Infinity和NaNn=9.0/0#=>Infinityn.class#=>Floatm=0/0.0#=>NaNm.class#=>Float但是当我想直接访问Infinity或NaN时:Infinity#=>uninitializedconstantInfinity(NameError)NaN#=>uninitializedconstantNaN(NameError)什么是Infinity和NaN?它们是对象、关键字还是其他东西? 最佳答案 您看到打印为Infinity和NaN的只是Float类的两个特殊实例的字符串

  2. ruby - 检查方法参数的类型 - 2

    我不确定传递给方法的对象的类型是否正确。我可能会将一个字符串传递给一个只能处理整数的函数。某种运行时保证怎么样?我看不到比以下更好的选择:defsomeFixNumMangler(input)raise"wrongtype:integerrequired"unlessinput.class==FixNumother_stuffend有更好的选择吗? 最佳答案 使用Kernel#Integer在使用之前转换输入的方法。当无法以任何合理的方式将输入转换为整数时,它将引发ArgumentError。defmy_method(number)

  3. ruby - Ruby 有 `Pair` 数据类型吗? - 2

    有时我需要处理键/值数据。我不喜欢使用数组,因为它们在大小上没有限制(很容易不小心添加超过2个项目,而且您最终需要稍后验证大小)。此外,0和1的索引变成了魔数(MagicNumber),并且在传达含义方面做得很差(“当我说0时,我的意思是head...”)。散列也不合适,因为可能会不小心添加额外的条目。我写了下面的类来解决这个问题:classPairattr_accessor:head,:taildefinitialize(h,t)@head,@tail=h,tendend它工作得很好并且解决了问题,但我很想知道:Ruby标准库是否已经带有这样一个类? 最佳

  4. ruby - 查找字符串中的内容类型(数字、日期、时间、字符串等) - 2

    我正在尝试解析一个CSV文件并使用SQL命令自动为其创建一个表。CSV中的第一行给出了列标题。但我需要推断每个列的类型。Ruby中是否有任何函数可以找到每个字段中内容的类型。例如,CSV行:"12012","Test","1233.22","12:21:22","10/10/2009"应该产生像这样的类型['integer','string','float','time','date']谢谢! 最佳答案 require'time'defto_something(str)if(num=Integer(str)rescueFloat(s

  5. ruby-on-rails - 在 Rails 开发环境中为 .ogv 文件设置 Mime 类型 - 2

    我正在玩HTML5视频并且在ERB中有以下片段:mp4视频从在我的开发环境中运行的服务器很好地流式传输到chrome。然而firefox显示带有海报图像的视频播放器,但带有一个大X。问题似乎是mongrel不确定ogv扩展的mime类型,并且只返回text/plain,如curl所示:$curl-Ihttp://0.0.0.0:3000/pr6.ogvHTTP/1.1200OKConnection:closeDate:Mon,19Apr201012:33:50GMTLast-Modified:Sun,18Apr201012:46:07GMTContent-Type:text/plain

  6. 电脑0x0000001A蓝屏错误怎么U盘重装系统教学 - 2

      电脑0x0000001A蓝屏错误怎么U盘重装系统教学分享。有用户电脑开机之后遇到了系统蓝屏的情况。系统蓝屏问题很多时候都是系统bug,只有通过重装系统来进行解决。那么蓝屏问题如何通过U盘重装新系统来解决呢?来看看以下的详细操作方法教学吧。  准备工作:  1、U盘一个(尽量使用8G以上的U盘)。  2、一台正常联网可使用的电脑。  3、ghost或ISO系统镜像文件(Win10系统下载_Win10专业版_windows10正式版下载-系统之家)。  4、在本页面下载U盘启动盘制作工具:系统之家U盘启动工具。  U盘启动盘制作步骤:  注意:制作期间,U盘会被格式化,因此U盘中的重要文件请注

  7. 【鸿蒙应用开发系列】- 获取系统设备信息以及版本API兼容调用方式 - 2

    在应用开发中,有时候我们需要获取系统的设备信息,用于数据上报和行为分析。那在鸿蒙系统中,我们应该怎么去获取设备的系统信息呢,比如说获取手机的系统版本号、手机的制造商、手机型号等数据。1、获取方式这里分为两种情况,一种是设备信息的获取,一种是系统信息的获取。1.1、获取设备信息获取设备信息,鸿蒙的SDK包为我们提供了DeviceInfo类,通过该类的一些静态方法,可以获取设备信息,DeviceInfo类的包路径为:ohos.system.DeviceInfo.具体的方法如下:ModifierandTypeMethodDescriptionstatic StringgetAbiList​()Obt

  8. kvm虚拟机安装centos7基于ubuntu20.04系统 - 2

    需求:要创建虚拟机,就需要给他提供一个虚拟的磁盘,我们就在/opt目录下创建一个10G大小的raw格式的虚拟磁盘CentOS-7-x86_64.raw命令格式:qemu-imgcreate-f磁盘格式磁盘名称磁盘大小qemu-imgcreate-f磁盘格式-o?1.创建磁盘qemu-imgcreate-fraw/opt/CentOS-7-x86_64.raw10G执行效果#ls/opt/CentOS-7-x86_64.raw2.安装虚拟机使用virt-install命令,基于我们提供的系统镜像和虚拟磁盘来创建一个虚拟机,另外在创建虚拟机之前,提前打开vnc客户端,在创建虚拟机的时候,通过vnc

  9. ruby - 在没有基准或时间的情况下用 Ruby 测量用户时间或系统时间 - 2

    因为我现在正在做一些时间测量,我想知道是否可以在不使用Benchmark类或命令行实用程序time的情况下测量用户时间或系统时间。使用Time类只显示挂钟时间,而不显示系统和用户时间,但是我正在寻找具有相同灵active的解决方案,例如time=TimeUtility.now#somecodeuser,system,real=TimeUtility.now-time原因是我有点不喜欢Benchmark,因为它不能只返回数字(编辑:我错了-它可以。请参阅下面的答案。)。当然,我可以解析输出,但感觉不对。*NIX系统的time实用程序也应该可以解决我的问题,但我想知道是否已经在Ruby中实

  10. ruby - 以毫秒为单位获取当前系统时间 - 2

    在Ruby中,以毫秒为单位获取自纪元(1970)以来的当前系统时间的正确方法是什么?我试过了Time.now.to_i,好像不是我想要的结果。我需要结果显示毫秒并且使用long类型,而不是float或double。 最佳答案 (Time.now.to_f*1000).to_iTime.now.to_f显示包含十进制数字的时间。要获得毫秒数,只需将时间乘以1000。 关于ruby-以毫秒为单位获取当前系统时间,我们在StackOverflow上找到一个类似的问题:

随机推荐