百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 编程网 > 正文

深度分析C#中Array的存储结构 c# array用法

yuyutoo 2024-10-12 00:45 6 浏览 0 评论

  数组是C#中最基础的存储结构之一,很多的存储结构其底层的实现中都是基于数组实现的,如:List、Queue、Stack、Dictionary、Heap等等,如果大家读过这些类型的底层实现源码,其实就可以发现,这些存储结构都是在其内部维护了一个或多个数组。本文重点来学习一下数组存储结构的实现逻辑。

  首先,我们来看看数组的定义:静态数组是由相同类型的元素线性排列的数据结构,在计算机上会分配一段连续的内存,对元素进行顺序存储。从以上的描述中,我们可以发现几个征:"相同类型、连续内存、顺序存储",这样的结构特性,可以能做到基于下标,对数组进行 O(1) 时间复杂度的快速随机访问。

  那么数组为什么可以做到快速随机访问?我们可以先来简单的说明一下,"存储数组时,会事先分配一段连续的内存空间,将数组元素依次存入内存。因为数组元素的类型都是一样的,所以每个元素占用的空间大小也是一样的,这样我们就很容易用“数组的开始地址 +index* 元素大小”的计算方式,快速定位到指定索引位置的元素,这也是数组基于下标随机访问的复杂度为 O(1) 的原因。"这样的描述可能是绝大部分同学都有所解除到的内容,并且也能让大家大致的存储原理,但是C#数组的存储结构是如何具体实现的呢?

  本文将从一个数组的基础操作开始,逐步来推导数组的在C#基础操作、数组在CoreCLR的维护策略,数组在C++的内存分配等阶段具体是如何实现的。

  首先,我们先来看一个简单的数组定义、初始化、赋值、取值的过程。

1         int[] myIntArray = new int[5] { 1, 2, 3, 4, 5 };
2 
3         for (int j = 0; j < 10; j++ )
4         {
5            Console.WriteLine("Element[{0}] = {1}", j, myIntArray[j]);
6         }

  这个过程中具体的实现逻辑什么样的呢,对于C#数组在内存的存储方式、数组的Cpoy、动态数组的扩容机制是什么样的呢?在C#中Array充当数组的基类,用于创建、处理、搜索数组并对数组进行排序,但是只有系统和编译器才能显式从 Array 类派生。接下来我们就来了解一下Array底层源码实现。对于数组的初始化,我们使用以上示例中的int[]进行介绍。在C#中所有的数组类型都集成自抽象类Array,在对int[]初始化的过程中,都会使用array的CreateInstance()方法,该方法存在多个重载,主要区别为用于创建一维、二维、三维等不同维数的数组结构,以下我们来看一下对于一维数据的创建代码。

1         public static unsafe Array CreateInstance(Type elementType, int length)
2         {
3             RuntimeType? t = elementType.UnderlyingSystemType as RuntimeType;
4 
5             return InternalCreate(t, 1, &length, null);
6         }

  上面的代码中,我们可以发现两个地方需要关注,第一部分:RuntimeType? t = elementType.UnderlyingSystemType as RuntimeType;该方法获取数组元素类型的基础系统类型,并将其转换为 RuntimeType。第二部分:InternalCreate(t, 1, &length, null)具体创建数组的操作,我们来看一下其实现的源码。(源码进行部分删减)

 1         private static unsafe Array InternalCreate(RuntimeType elementType, int rank, int* pLengths, int* pLowerBounds)
 2         {
 3             if (rank == 1)
 4             {
 5                 return RuntimeImports.RhNewArray(elementType.MakeArrayType().TypeHandle.ToEETypePtr(), pLengths[0]);
 6             }
 7             else
 8             {
 9                 int* pImmutableLengths = stackalloc int[rank];
10                 
11                 for (int i = 0; i < rank; i++) pImmutableLengths[i] = pLengths[i];
12 
13                 return NewMultiDimArray(elementType.MakeArrayType(rank).TypeHandle.ToEETypePtr(), pImmutableLengths, rank);
14             }
15         }

  该方法用于在运行时创建数组,其中参数elementType表示数组元素运行时的类型,rank表示数组的维度,pLengths表示指向数组长度的指针,pLowerBounds表示指向数组下限(如果有的话)的指针。根据设定的rank的值,创建一维或多维数组。其中elementType.MakeArrayType().TypeHandle.ToEETypePtr()表示先将当前type 对象表示的类型通过 MakeArrayType 方法创建一个数组类型,然后获取该数组类型的运行时类型句柄,最后通过 ToEETypePtr 方法将运行时类型句柄转换为指向类型信息的指针。我们先看一下创建一维数组的逻辑,具体代码如下:

1         [MethodImpl(MethodImplOptions.InternalCall)]

2         [RuntimeImport(RuntimeLibrary, "RhNewArray")]
3         private static extern unsafe Array RhNewArray(MethodTable* pEEType, int length);
4 
5         internal static unsafe Array RhNewArray(EETypePtr pEEType, int length) => RhNewArray(pEEType.ToPointer(), length);

  该方法是具体实现数组创建的逻辑,我们先来看一下参数,其中EETypePtr是CLR中用于表示对象类型信息的指针类型。每个.NET对象在运行时都关联有一EEType结构,它包含有关对象类型的信息,例如该类型的方法表、字段布局、基类信息等。

  这里简单的介绍一下代码上面的两个自定义属性:

(1)、[MethodImpl(MethodImplOptions.InternalCall)] 
   指示编译器生成的方法体会被一个外部实现取代,而该外部实现通常由运行时环境提供。

(2)、[RuntimeImport(RuntimeLibrary, "RhNewArray")] 
    这是一个自定义的特性,在项目中定义的用于指示运行时导入的特性。 

在C#中,使用属性标记运行时导入的位置通常是为了提供额外的元数据和信息,以告诉编译器和运行时环境如何正确地处理外部方法的调用。

  使用属性标记运行时导入的主要目的有以下几点:

(1)、元数据信息:运行时导入的位置可能包括一些元数据信息,如函数名称、库名称、调用约定等。 
    使用属性可以将这些信息嵌入到C#代码中,使得代码更加自解释,并提供足够的信息供编译器和运行时使用。 

(2)、优化和安全性:编译器和运行时环境可能会使用属性来进行性能优化或安全性检查。 例如,通过指定调用约定或其他属性,可以帮助编译器生成更有效的代码。
 
(3)、与运行时环境交互:属性可以提供一种与底层运行时环境进行交互的机制。 例如,通过自定义属性,可以向运行时环境传递一些特殊的标志或信息,以影响方法的行为。

(4)、代码维护和可读性:使用属性可以提高代码的可维护性和可读性。 在代码中使用属性来标记运行时导入的位置,使得代码的意图更加清晰,也有助于团队协作。

  在CLR的内部,EETypePtr是一个指向EEType结构的指针,其中EEType是运行时中用于描述对象类型的结构。EEType结构的内容由运行时系统生成和管理,而EETypePtr则是对这个结构的指针引用。根据传入的运行时对象类型进行处理,我们接下来看一下pEEType.ToPointer()的实现。

1         internal unsafe Internal.Runtime.MethodTable* ToPointer()
2         {
3             return (Internal.Runtime.MethodTable*)(void*)_value;
4         }

  ToPointer()方法目的是将其对象或值转换为指针,MethodTable 是CLR用于管理类和对象的元数据,用于存储类型相关信息的数据结构,每个对象在内存中都包含一个指向其类型信息的指针,这个指针指向该类型的 MethodTable,用于支持CLR在运行时进行类型检查、虚方法调用等操作。那我们来具体看一下MethodTable的数据结构。

 1         struct MethodTable
 2         {
 3             // 指向类型的虚方法表(VTable)
 4             IntPtr* VirtualMethodTable;
 5 
 6             // 字段表
 7             FieldInfo* Fields;
 8 
 9             // 接口表
10             InterfaceInfo* Interfaces;
11 
12             // 其他元数据信息...
13         }

  我们从原始的数组初始化和赋值,一直推导至对象的数组空间维护。截止当前,我们获取到数组的MethodTable* pEEType数据结构。接下来我们来看一下CLR对数组的内存空间分配逻辑和维护方案。由于CoreCLR中的实现代码我们没有办法全面的了解,我们接下按照预定的逻辑进行一定的推论。(CCoreCLR的实现代码绝大部分是使用C++实现)

 1 #include <cstdint>
 2 
 3 extern "C" {
 4     struct MethodTable { // 方法表等信息...};
 5     struct Array { // 数组相关信息...};
 6     void* RhNewArray(void* pEEType, int length) {
 7         // 假设存在一个用于对象分配的函数,该函数分配数组的内存
 8         void* rawArrayMemory = AllocationFunction(length * sizeof(Array));
 9         // 将传递的 pEEType 信息保存到数组对象中
10         Array* newArray = static_cast<Array*>(rawArrayMemory);
11         //为数组对象设置元数据信息
12         newArray->MethodTablePointer = pEEType;
13         return rawArrayMemory;
14     }
15 }

  以上代码是一种假设实现方式, AllocationFunction 的函数用于内存分配,并且数组对象(Array)有一个成员 MethodTablePointer 用于存储 MethodTable 的指针。接下来我们再来看一下AllocationFunction()方法推测实现逻辑。

1 void* AllocationFunction(size_t size) {
2     // 使用标准库的 malloc 函数进行内存分配
3     void* memory = malloc(size);
4     //处理内存分配失败的情况
5     ...
6     return memory;
7 }

  以上的代码中,使用标准函数库malloc()进行内存的分配,malloc ()是C标准库中的一个函数,用于在运行时动态分配内存。malloc ()接受一个 size 参数,表示要分配的内存字节数。它返回一个指向分配内存起始地址的指针,或者在分配失败时返回 NULL。malloc ()内存分配逻辑通常涉及以下步骤:

(1)、请求内存空间: malloc() 根据传递的 size 参数向系统请求一块足够大的内存空间。 

(2)、内存分配:如果系统成功分配了请求的内存块,malloc 会在这块内存中标记已分配的部分,并将其起始地址返回给调用者。 

(3)、返回结果:如果分配成功,malloc 返回一个指向新分配内存的指针。如果分配失败(例如,系统内存不足),则返回 NULL。

(4)、内存对齐:部分系统要求分配的内存是按照特定字节对齐的。因此,malloc 通常会确保返回的内存地址满足系统的对齐要求。 

(5)、初始化内存:malloc 返回的内存通常不会被初始化,即其中的数据可能是未知的。在使用之前,需要通过其他手段对内存进行初始化。 

(6)、内存管理:一些实现可能会使用内部数据结构来跟踪已分配和未分配的内存块,以便在 free 被调用时能够释放相应的内存。

  以上简单的描述了C++在底层实现内存分配的简单实现方式,对于CoreCLRe中对于数组的内存空间申请相对非常复杂,可能涉及内存池、分配策略、对齐要求等方面的考虑。后续有机会再做详细的介绍。既然说到CoreCLR的内存实现为C++的内存分配策略,那我们接下来看一下其对应的常用策略管理策略。我们用一个简单的数组的内存分配。

1 int myArray[5]; // 声明一个包含5个整数的数组
2 
3 +------+------+------+------+------+
4 | int0 | int1 | int2 | int3 | int4 |
5 +------+------+------+------+------+

  myArray 是整个数组的起始地址,然后每个 int 元素按照其大小排列在一起。基于以上的分析,我们可以看到C++对于内存的分配概述大致如下:

(1)、元素的内存布局:数组的元素在内存中是依次排列的,每个元素占用的内存空间由元素的类型决定。 
    例如,一个 int 数组中的每个整数元素通常占用4个字节(32位系统)或8个字节(64位系统)。 

(2)、数组的起始地址:数组的内存分配通常从数组的第一个元素开始。数组的起始地址是数组第一个元素的地址。 

(3)、连续存储:数组的元素在内存中是连续存储的,这意味着数组中的每个元素都直接跟在前一个元素的后面。

  上面介绍了内存空间的分配,我们接下来看一下这段代码的实现逻辑,rawArrayMemory: 这是一个 void* 类型的指针,通常指向分配的内存块的起始位置。static_cast 运算符,将 rawArrayMemory 从 void* 类型转换为 Array* 类型。

1  Array* newArray = static_cast<Array*>(rawArrayMemory);

  我们从以上对于数组的创建过程中,分析了C#、CoreCLR、C++等多个实现视角进行了简单的分析。

  接下来我们回归到CoreCLR中对于数组的内存空间管理策略,数组内存分配的常用步骤:

1、分配对象头:为数组对象分配对象头,对象头包含一些元数据,如类型指针、同步块索引等信息。 

2、分配数组元素空间:分配存储数组元素的内存块,这是实际存储数组数据的地方。 

3、初始化数组元素:根据数组类型的要求,初始化数组元素。这可能涉及到对元素进行默认初始化,例如将整数数组的每个元素初始化为零。 

4、返回数组引用:返回指向数组对象的引用,使得该数组可以被使用。

  当我们在托管代码中声明一个数组时,CoreCLR会在托管堆上动态分配内存,以存储数组的元素,并在分配的内存块中存储有关数组的元数据,这些元数据通常包括数组的长度和元素类型等信息。CoreCLR通常会对分配的内存进行对齐,以提高访问效率,这可能导致分配的内存块略大于数组元素的实际大小。可能有同学会问为什么要进行内存的对齐,这里就简单的说明一下。

1、硬件要求:访问特定类型的数据时,其地址应该是某个值的倍数。 

2、提高访问速度:对齐的内存访问通常比不对齐的访问更加高效。处理器通常能够更快地访问对齐的内存,因为这符合硬件访问模式。 

3、减少内存碎片:内存对齐还有助于减少内存碎片,使得内存的使用更加紧凑。内存碎片可能导致性能下降,因为它可能增加了分配和释放内存的开销。 

4、硬件事务:一些处理器和操作系统支持原子操作,但通常要求数据是按照特定的对齐方式排列的。

  上面介绍了为什么需要进行内存对齐,那么对于CoreCLR的内部实现是如何进行内存对齐的呢?我们简洁的介绍一下实现大流程:

1、使用操作系统的内存分配函数:使用操作系统提供的内存分配函数来分配托管堆上的内存。在Windows上可能是HeapAlloc。 

2、对齐方式的指定:在调用内存分配函数时,会指定所需的对齐方式。通常是以字节为单位的对齐值。常见的对齐值包括4字节、8字节等。 

3、内存块的对齐:内存分配函数返回的内存块通常是按照指定的对齐方式进行对齐的。CLR确保返回的内存块的起始地址符合对齐规则。 

4、对齐规则的维护:维护对齐规则的信息,确保在托管堆上分配和释放的内存块都符合相同的对齐方式。 

5、内存对齐的优化:对内存对齐进行一些优化,以提高访问效率。例如,它可以在对象的布局中考虑对齐规则,以减少内存碎片。

  具体的数组内存分配策略可能会因CLR的版本和实现而异。不同的垃圾回收算法(如标记-清除、复制、标记-整理等)以及不同的GC代(新生代、老年代)也可能影响内存分配的具体实现。在.NET中,CLR提供了不同的垃圾回收器实现,例如Workstation GC和Server GC。Workstation GC通常适用于单处理器或少量处理器的环境,而Server GC适用于多处理器环境。这些GC实现可能在内存分配和回收方面有一些差异。

  本文借助了一个数组的初始化和赋值为样例,逐层的分析了数组对象运行时结构的获取、对象MethodTable结构的分析、CoreCLR底层对数组内存结构的创建推导、C++对于内存的分配策略等视角,最后还综合的介绍了CLRCore对于数组内存的创建步骤。

  我们一直以来对于数组的内存分配,都有一个整体的认识,其特点是"相同类型、连续内存、顺序存储",对于其连续内存的特点记忆深刻,但是在内部如何实现进行的连续内存却没有整体的了解,C#内部是如何完成不同类型对象数组的运行时创建,在CoreCLR内部如何进行内存的划分是没有做过了解和推导,甚至于CoreCLR内部是如何维护一个对象的结构,很多时候都只是了解到运行时对象使用Type类型就可以得到,那么CoreCLR内部如何来维护这个Type呢?其实很多时候没有特点去了解过其结构。

  以上内容是对C#中Array的存储结构的简单介绍,如错漏的地方,还望指正。

相关推荐

史上最全的浏览器兼容性问题和解决方案

微信ID:WEB_wysj(点击关注)◎◎◎◎◎◎◎◎◎一┳═┻︻▄(页底留言开放,欢迎来吐槽)●●●...

平面设计基础知识_平面设计基础知识实验收获与总结
平面设计基础知识_平面设计基础知识实验收获与总结

CSS构造颜色,背景与图像1.使用span更好的控制文本中局部区域的文本:文本;2.使用display属性提供区块转变:display:inline(是内联的...

2025-02-21 16:01 yuyutoo

写作排版简单三步就行-工具篇_作文排版模板

和我们工作中日常word排版内部交流不同,这篇教程介绍的写作排版主要是用于“微信公众号、头条号”网络展示。写作展现的是我的思考,排版是让写作在网格上更好地展现。在写作上花费时间是有累积复利优势的,在排...

写一个2048的游戏_2048小游戏功能实现

1.创建HTML文件1.打开一个文本编辑器,例如Notepad++、SublimeText、VisualStudioCode等。2.将以下HTML代码复制并粘贴到文本编辑器中:html...

今天你穿“短袖”了吗?青岛最高23℃!接下来几天气温更刺激……

  最近的天气暖和得让很多小伙伴们喊“热”!!!  昨天的气温到底升得有多高呢?你家有没有榜上有名?...

CSS不规则卡片,纯CSS制作优惠券样式,CSS实现锯齿样式

之前也有写过CSS优惠券样式《CSS3径向渐变实现优惠券波浪造型》,这次再来温习一遍,并且将更为详细的讲解,从布局到具体样式说明,最后定义CSS变量,自定义主题颜色。布局...

柠檬科技肖勃飞:大数据风控助力信用社会建设

...

你的自我界限够强大吗?_你的自我界限够强大吗英文

我的结果:A、该设立新的界限...

行内元素与块级元素,以及区别_行内元素和块级元素有什么区别?

行内元素与块级元素首先,CSS规范规定,每个元素都有display属性,确定该元素的类型,每个元素都有默认的display值,分别为块级(block)、行内(inline)。块级元素:(以下列举比较常...

让“成都速度”跑得潇潇洒洒,地上地下共享轨交繁华
让“成都速度”跑得潇潇洒洒,地上地下共享轨交繁华

去年的两会期间,习近平总书记在参加人大会议四川代表团审议时,对治蜀兴川提出了明确要求,指明了前行方向,并带来了“祝四川人民的生活越来越安逸”的美好祝福。又是一年...

2025-02-21 16:00 yuyutoo

今年国家综合性消防救援队伍计划招录消防员15000名

记者24日从应急管理部获悉,国家综合性消防救援队伍2023年消防员招录工作已正式启动。今年共计划招录消防员15000名,其中高校应届毕业生5000名、退役士兵5000名、社会青年5000名。本次招录的...

一起盘点最新 Chrome v133 的5大主流特性 ?

1.CSS的高级attr()方法CSSattr()函数是CSSLevel5中用于检索DOM元素的属性值并将其用于CSS属性值,类似于var()函数替换自定义属性值的方式。...

竞走团体世锦赛5月太仓举行 世界冠军杨家玉担任形象大使

style="text-align:center;"data-mce-style="text-align:...

学物理能做什么?_学物理能做什么 卢昌海

作者:曹则贤中国科学院物理研究所原标题:《物理学:ASourceofPowerforMan》在2006年中央电视台《对话》栏目的某期节目中,主持人问过我一个的问题:“学物理的人,如果日后不...

你不知道的关于这只眯眼兔的6个小秘密
你不知道的关于这只眯眼兔的6个小秘密

在你们忙着给熊本君做表情包的时候,要知道,最先在网络上引起轰动的可是这只脸上只有两条缝的兔子——兔斯基。今年,它更是迎来了自己的10岁生日。①关于德艺双馨“老艺...

2025-02-21 16:00 yuyutoo

取消回复欢迎 发表评论: