数组和CLR-非常特殊的关系
数组和CLR-非常特殊的关系
原文地址:https://mattwarren.org/2017/05/08/Arrays-and-the-CLR-a-Very-Special-Relationship/
译文作者:杰哥很忙
前段时间,我写了关于字符串和CLR之间的"特殊关系",事实证明,Array 和 CLR 具有更深的关系。
顺便说一下,如果你喜欢阅读CLR本质类的文章,你可能会对这些文章很有趣:
公共语言运行时(CLR)的基础
数组是 CLR 的基本部分,它们包含在 ECMA 规范中,以明确运行时必须实现数组:

另外,还有一些专门处理数组的IL(中间语言)指令:
newarr <etype>
创建一个元素类型为etype的数组ldelem.ref
将位于指定数组索引处的包含对象引用的元素作为 O 类型(对象引用)加载到计算堆栈的顶部,O类型与在 CIL 堆栈上推送的数组的元素类型相同。stelem <typeTok>
堆栈上的值保存到数组索引位置(stelem.i、stelem.i1、stelem.i2、stelem.r4等类似)ldlen
将数组长度推到堆栈(本机无符号int类型)
有专用IL指令非常有意义,因为数组是很多其他数据类型的构建基块,你会你希望它能在 C# 等现代高级语言中可用、定义良好且高效。如果没有数组,就不可能有列表、字典、队列、堆栈、树等数据结构,它们都是构建在数组之上的,这些数组以类型安全的方式提供对连续内存片段的低级访问。
内存和类型安全
这种内存和类型安全很重要,因为没有它,.NET就不能被描述为"托管运行时",并且当你以更低级的语言编写代码时,你必须自己处理所遇到的类型安全的问题。
更具体地说,CLR 在使用数组时提供以下保护(来自BORT的"CLR 简介"页面中有关内存和类型安全部分):
GC 是保证内存安全的必要条件,但不充分。GC 并不会禁止程序越界访问数组,或是越界访问一个对象的成员(如果你通过基地址和偏移来计算成员地址的话)。不过,如果我们有办法解决这些问题,我们就能够实现内存安全的程序。
公共中间语言(CIL)确实 提供了一些操作符,它们可以用来在任意内存上读取和写入数据(因此破坏了内存安全性),不过他还提供了下面这些内存安全的操作符,CLR 也强烈建议在大多数的情况下使用它们:
- 字段访问操作符(LDFLD、STFLD、LDFLDA),它们能够通过名字来读取、写入一个字段,以及获取一个字段的地址。
- 数组访问操作符(LDELEM、STELEM、LDELEMA),它们能够通过数据索引来读取、设置数组元素,以及获取数组元素的地址。所有的数组都有一个标签,写明了数组的长度。在每次访问数组元素时,都会自动进行边界检查。
译者补充:BORT是Book Of The Runtime的缩写。
对于完整的IL指令可以点击这里查看
在《托管数组结构》一文中有介绍数组的布局,在类型句柄之后是数组长度。
此外,从BOTR页的可验证代码-强制内存和类型安全"一节中提到:
事实上,需要运行时检查的数量实际上是非常小的。它们包括以下操作:
- 将指向基类型的指针转换为为指向派生类型的指针(可以静态检查相反的操作)
- 数组边界检查(正如我们看到的内存安全性一样)
- 将指针数组中的一个元素赋值为一个新的(指针)值。需要这种检查的原因是,CLR 的数组支持自由转换规则(后文会详细介绍)
但是,并不能免费获得类型安全保护,需要有一些性能开销:
需要注意的是,需要执行这些检查会对运行时进行要求。特别是:
- GC堆中的所有内存必须标记类型(以便可以实现强制转换运算符)。在运行时类型信息必须可以被获取到,而且必须包含足够的信息来确定类型转换是否合法(例如,运行时需要知道继承层次结构)。实际上,GC 堆上每个对象中的第一个字段指向表示其类型的运行时数据结构。
- 所有数组还必须有大小字段(用于边界检查)。
- 数组必须包含有关其元素类型的完整类型信息。
译者补充:在《托管数组结构》一文中介绍了托管数组的布局结构。第一个字段指向其类型的运行时数据结构指的是类型句柄,指向的是该对象的方法表。
实现细节
事实证明,数组大部分的内部实现最好描述为“魔术”,Stack Overflow 的来自 Marc Gravell的回答很好的总结了它。
数组基本上是使用了“魔术”。因为它们早于泛型,但必须允许动态类型创建(即使在.NET 1.0中也需要),因此它们通过使用一些技巧、黑客等手段实现。
是的,数组在泛型存在之前就被参数化(即通用化)。这意味着你可以在编写 List<int> 或 List<string> 之前创建数组(如 int[] 和 string[]),这仅在 .NET 2.0 中成为可能。
特殊帮助器类
所有这些魔术或技巧可能造成两件事:
- CLR 违反了所有常见的类型安全规则
- 特殊的数组帮助器类叫做
SZArrayHelper
但首先,为什么需要这些技巧?来自《.NET Arrays, IList, Generic Algorithms, and what about STL?》:
当我们设计泛型集合类时,困扰我的一件事就是如何编写一个通用算法,能处理数组和集合。当然,为了驱动泛型编程,我们必须尽可能使数组和泛型集合无缝衔接。应该有一个简单的解决方案来解决这个问题,这意味着你必编写相同的代码两次,一次使用
IList<T>,一次使用T[]。 我突然意识到的解决方案是数组需要实现我们的通用 IList。我们在 .Net Framework 1.1 中使数组实现了非泛型 IList,由于缺少 IList 强类型和所有数组(System.Array)的基类,所以实现相当简单。我们需要的是以强类型方式为IList<T>执行相同的操作。
但它只针对常见情况(即"单维"数组):
不过,这里存在一些限制,我们不想支持多维数组,因为
IList<T>只提供单维访问。 此外,具有非零下限的数组相当奇怪,并且可能不能很好的与IList<T>相匹配,因为大多数人可能会从 IList 的0到 Count 进行遍历。因此,我们不是使System.Array实现IList<T>,使T[]实现IList<T>。 在这里,T[] 表示以 0 为下限的单维数组(通常在内部称为 SZArray,但我认为 Brad 希望在某个时间点公开推广术语"矢量"),并且元素类型为 T。因此,Int32[]实现IList<Int32>,string[]实现了IList<String>。
此外,数组源代码中的此注释进一步阐明了原因:
//----------------------------------------------------------------------------------
// Calls to (IList<T>)(array).Meth are actually implemented by SZArrayHelper.Meth<T>
// This workaround exists for two reasons:
//
// - For working set reasons, we don't want insert these methods in the array
// hierachy in the normal way.
// - For platform and devtime reasons, we still want to use the C# compiler to
// generate the method bodies.
//
// (Though it's questionable whether any devtime was saved.)
//
// ....
//----------------------------------------------------------------------------------
因此,这样做是为了方便和高效,因为他们不希望 System.Array 的每个实例都携带 IEnumera<T> 和 IList<T> 实现的所有代码。
此映射通过调用 GetActualImplementationForArrayGenericIListOrIReadOnlyListMethod(..),。它负责从 SZArrayHelper 连接相应的方法类,即 IList<T>.Count -> SZArrayHelper.Count<T>,或者如果该方法是 IEnumerator<T> 接口的一部分,则使用 SZGenericArrayenumerator。
但是,这有可能导致安全漏洞,因为它打破了正常的 C# 类型系统保证,特别是关于this指针。为了说明这个问题,下面是 Count 属性的源代码,请注意对 JitHelpers.UnsafeCast<T[]> 的调用。
internal int get_Count<T>()
{
//! Warning: "this" is an array, not an SZArrayHelper. See comments above
//! or you may introduce a security hole!
T[] _this = JitHelpers.UnsafeCast<T[]>(this);
return _this.Length;
}
它必须重新映射 this,以便能够调用正确的对象的 length
译者补充:正如上面的注释所描述,
get_Count<T>是SZArrayHelper中的实例方法,而this并不是指SZArrayHelper,而是指数组。
以防这些注释描述的不够,在这个类的顶部有一个措辞非常强烈的注释,进一步阐明了风险!!
一般来说,所有这些“魔术”都是隐藏的,但偶尔它会向外暴露出来。例如,如果你运行以下代码,SZArrayHelper 将显示在StackTrace和TargetSite中:
try {
int[] someInts = { 1, 2, 3, 4 };
IList<int> collection = someInts;
// Throws NotSupportedException 'Collection is read-only'
collection.Clear();
} catch (NotSupportedException nsEx) {
Console.WriteLine("{0} - {1}", nsEx.TargetSite.DeclaringType, nsEx.TargetSite);
Console.WriteLine(nsEx.StackTrace);
}
System.SZArrayHelper - Void Clear[T]()
在 System.SZArrayHelper.Clear[T]()
移除边界检查
运行时还以更传统的方式为数组提供了支持,其中第一种与性能有关。数组边界检查提供了很好的内存安全,但它们有开销成本,因此,若有可能,JIT 会删除任何它所知道的冗余检查。
它通过计算for循环访问的值范围并将这些值与数组的实际长度进行比较来实现该功能。如果它确定从未尝试访问数组边界以外的项,就会删除运行时检查。
更多详细信息,以下链接将带你到处理此情况的 JIT 源代码:
- JIT尝试移除范围检查
- RangeCheck::OptimizeRangeCheck(..)
- 接着调用RangeCheck::GetRange(..)
- 然后调用Compiler::optRemoveRangeCheck(..)删除范围检查
- 非常有用的源代码注释解释范围检查删除逻辑
如果你感兴趣,看看这里,我将数组检索边界检查的“删除”和“不删除”的场景放到了一起。
分配数组
运行时所提供帮助的另一个任务是使用手写的程序集代码分配数组,以便尽可能优化方法,请参阅:
运行时以不同的方式对待数组
最后,由于数组与 CLR 紧密联系在一起,因此在很多地方,它们都被作为特殊情况进行处理。例如,在 CoreCLR 源码中搜索IsArray()会返回超过 60 条记录,包括:
- 数组的方法表的生成方式不同
- 当你调用数组的
ToString()方法时, 你会获取到一个特别的格式,比如System.Int32[]或MyClass[,]
所以,公平地说,数组和CLR有一个非常特殊的关系
进一步阅读
和往常一样,这里有一些更多的链接提供给你阅读
- CSharp Specification for Arrays
- .NET Type Internals - From a Microsoft CLR Perspective - ARRAYS
- CLR INSIDE OUT - Investigating Memory Issues
- Internals of Array
- Internals of .NET Objects and Use of SOS
- Memory layout of .NET Arrays
- Memory Layout of .NET Arrays (x64)
- Why are multi-dimensional arrays in .NET slower than normal arrays?
- How do arrays in C# partially implement IList?
- Purpose of TypeDependencyAttribute(“System.SZArrayHelper”) for IList, IEnumerable and ICollection?
- What kind of class does ‘yield return’ return
- SZArrayHelper implemented in Shared Source CLI (SSCLI)
数组源码引用
参考文档
- StructLayout特性
- Compiling C# Code Into Memory and Executing It with Roslyn
- .NET CLR 运行原理
- Drill Into .NET Framework Internals to See How the CLR Creates Runtime Objects
- How do arrays in C# partially implement IList?
- .NET Arrays, IList, Generic Algorithms, and what about STL?
- 什么是 CLR ?
微信扫一扫二维码关注订阅号杰哥技术分享
出处:https://www.cnblogs.com/Jack-Blog/p/12360447.html
作者:杰哥很忙
本文使用「CC BY 4.0」创作共享协议。欢迎转载,请在明显位置给出出处及链接。
数组和CLR-非常特殊的关系的更多相关文章
- [CLR via C#]16. 数组
数组是允许将多个数据项当作一个集合来处理的机制.CLR支持一维数组.多维数组和交错数据(即由数组构成的数组).所有数组类型都隐式地从System.Array抽象类派生,后者又派生自System.Obj ...
- c/c++ 函数、常量、指针和数组的关系梳理
压力才有动力,15年中旬就要准备实习,学习复习学习复习学习复习学习复习……无限循环中,好记性不如烂笔头……从数组开始,为主干. c 的array由一系列的类型相同的元素构成,数组声明包括数组元素个数和 ...
- C指针和数组的关系详解
1.C中数组和指针的关系 对于任意类型的数组arr,对于同类型的指针类型parr(确切一点,可以假设类型为int,即int arr[], *parr).它们之间有如下"内幕": 1 ...
- 重温CLR(十一) 枚举类型、位标志和数组
枚举类型 枚举类型(enumerated types)定义了一组"符号名称/值"配对.例如,以下Color类型定义了一组符号,每个符号都标识一种颜色: internal enum ...
- C/C++中二维数组和指针关系分析
在C/c++中,数组和指针有着密切的关系,有很多地方说数组就是指针式错误的一种说法.这两者是不同的数据结构.其实,在C/c++中没有所谓的二维数组,书面表达就是数组的数组.我猜想是为了表述方便才叫它二 ...
- 1199 Problem B: 大小关系
求有限集传递闭包的 Floyd Warshall 算法(矩阵实现) 其实就三重循环.zzuoj 1199 题 链接 http://acm.zzu.edu.cn:8000/problem.php?id= ...
- PHP数组函数总结
array_change_key_case - 返回字符串键名全为小写或大写的数组 array_chunk - 将一个数组分割成多个 array_column - 返回数组中指定的一列 array_c ...
- About_PHP_数据类型&常用数组函数
PHP数据类型总结: 1:Boolean 布尔类型 返回值就是true和false 特殊情况:(1)false (2)整型0的时候 (3)空字符串/字符串“0” (4)null 以上都会被返回fals ...
- JAVA06数组之动手动脑问题解决
一.随机生成10个数,填充一个数组,然后用消息框显示数组内容,接着计算数组元素的和,将结果也显示在消息框中. 1.设计思路:首先生成10个随机数,然后存放至长度至少是10的数组中,然后计算10个随机 ...
随机推荐
- FileUpload的控件上传excel
在一个使用FileUpload的控件上传excel,读取excel的数据 因为上传的路径一直被限定在C:\Program\IIS\Express 一直限制这个文件下, 想要解决这个问题. 在谷歌浏览器 ...
- 如何利用Serilog的RequestLogging来精简ASP.NET Core的日志输出
这是该系列的第一篇文章:在ASP.NET Core 3.0中使用Serilog.AspNetCore. 第1部分-使用Serilog RequestLogging来简化ASP.NET Core的日志输 ...
- 【大道至简】NetCore3.1快速开发框架一:搭建框架
这一章,我们直接创建NetCore3.1的项目 主要分为1个Api项目,和几个类库 解释: 项目——FytSoa.Api:提供前端接口的Api项目 类库——FytSoa.Core:包含了数据库操作类和 ...
- iocp性能分析
网络上找iocp性能分析的文章很少,因工作关系,花了点时间特意从客观数据和理论角度分析了下iocp的性能 环境 CPU i7 4核8线程 1G网卡,echo方式测试(一个客户机模拟多个客户端模式,模拟 ...
- 机器学习环境配置系列二之cuDNN
1.下载cuDNN 前往: NVIDIA cuDNN home page. 进入下载 勾选Nvidia的协议复选框(流氓的选择,不勾选不能下载) 选择与安装的cuda版本一致的cudnn进行下载. 2 ...
- mysql数据库的备份与数据恢复
一.定时备份数据库 前段时间工作中搭建了HttpRunnerManager的接口自动化测试平台,由于平台中没有提供用例下载的功能及权限管理功能,自己也不会写前端,于是就想了办法,那就是备份数据库,如 ...
- File流与IO流 看这一篇就够了
主要内容 File类 递归 IO流 字节流 字符流 异常处理 Properties 缓冲流 转换流 序列化流 打印流 学习目标 [ ] 能够说出File对象的创建方式 [ ] 能够说出File类获取名 ...
- 微信小程序如何创建云函数并安装wx-server-sdk依赖
时间:2020/01/23 步骤 1.在微信开发者工具中云函数所在的文件夹的图标与其他文件夹是不同的,如下(第一个是云函数): 如果需要使一个普通文件变为云函数文件夹,需要在project.confi ...
- ubuntu18.04系统下无外部显示问题解决
记录一下自己作死过程. 由于学习的需要,在windows10下装了ubuntu18.04系统,第一次装这个系统时,也出现了无外部显示,那时候是老师帮忙搞好的,当时没太在意,只是走马关花的看了老师操作了 ...
- 九 Shell中的数组
数组:用一个变量存储一组数据,并能够对这组数据中的某一个数据单独操作. 数组的类型:一维数组.二维数组.多维数组 变量的类型 Shell中默认无类型 变量的值默认均视为文本 用在数字运算中时,自动将其 ...
