如何将字节序列转换成数组对象?
摘要:《.NET中的数组在内存中如何布局? 》介绍了一个.NET下针对数组对象的内存布局。既然我们知道了内存布局,我们自然可以按照这个布局规则创建一段字节序列来表示一个数组对象,就像《以纯二进制的形式在内存中绘制一个对象》构建一个普通的对象,以及
《.NET中的数组在内存中如何布局? 》介绍了一个.NET下针对数组对象的内存布局。既然我们知道了内存布局,我们自然可以按照这个布局规则创建一段字节序列来表示一个数组对象,就像《以纯二进制的形式在内存中绘制一个对象》构建一个普通的对象,以及《你知道.NET的字符串在内存中是如何存储的吗?》构建一个字符串对象一样。
一、数组类型布局
二、利用字节数组构建数组
三、利用非托管本地内存构建数组
四、性能测试
一、数组类型布局我们再简单回顾一下数组对象的内存布局。如下图所示,对于32位(x86)系统,Object Header和TypeHandle各占据4个字节;但是对于64位(x64)来说,存储方法表指针的TypeHandle自然扩展到8个字节,但是Object Header依然是4个字节,为了确保TypeHandle基于8字节的内存对齐,所以会前置4个字节的“留白(Padding)”。
其荷载内容(Payload)采用如下的布局:前置4个字节以UInt32的形式存储数组的长度,后面依次存储每个数组元素的内容。对于64位(x64)来说,为了确保数组元素的内存对齐,两者之间具有4个字节的Padding。
二、利用字节数组构建数组如下所示的BuildArray<T>方法帮助我们构建一个指定长度的数组,数组元素类型由泛型参数决定。如代码片段所示, 我们根据上述的内存布局规则计算出目标数组占据的字节数,并据此创建一个对应的字节数组来表示构建的数组。我们将数组类型(T[])的TypeHandle的值(方法表地址)写入对应的位置(偏移量和长度均为IntPtr.Size),紧随其后的4个字节写入数组的长度。自此一个指定元素类型/长度的空数组就已经构建出来了,我们让返回的数组变量指向数组的第IntPtr.Size个字节(4字节/8字节)。
unsafe static T[] BuildArray<T>(int length)
{
var byteCount =
IntPtr.Size // Object header + Padding
+ IntPtr.Size // TypeHandle
+ IntPtr.Size // Length + Padding
+ Unsafe.SizeOf<T>() * length // Elements
;
var bytes = new byte[byteCount];
Unsafe.Write(Unsafe.AsPointer(ref bytes[IntPtr.Size]), typeof(T[]).TypeHandle.Value);
Unsafe.Write(Unsafe.AsPointer(ref bytes[IntPtr.Size * 2]), length);
T[] array = null!;
Unsafe.Write(Unsafe.AsPointer(ref array), new IntPtr(Unsafe.AsPointer(ref bytes[IntPtr.Size])));
return array;
}接下来我们就来验证一下BuildArray<T>构建的数组是否可以正常使用。如下面的代码片段所示,我们调用这个方法构建了一个长度位100的整型数组,并利用调试断言确定构建的数组长度是否正常,并验证每个元素是否置空。接下来我们对每个数组元素赋值,并利用调试断言验证赋值是否有效。
var array = BuildArray<int>(100);
Debug.Assert(array.Length == 100);
Debug.Assert(array.All(it => it == 0));
for (int index = 0; index < array.Length; index++)
{
array[index] = index;
}
for (int index = 0; index < array.Length; index++)
{
Debug.Assert(array[index] == index);
}上面演示的是值类型(Int32)数组的构建,下面采用类似的形式构建了一个引用类型(String)的数组。
