利用一段字節序列構建一個數組對象

.NET中的數組在內存中如何佈局? 》介紹了一個.NET下針對數組對象的內存佈局。既然我們知道了內存佈局,我們自然可以按照這個佈局規則創建一段字節序列來表示一個數組對象,就像《以純二進制的形式在內存中繪製一個對象》構建一個普通的對象,以及《你知道.NET的字符串在內存中是如何存儲的嗎?》構建一個字符串對象一樣。

一、數組類型佈局
二、利用字節數組構建數組
三、利用非託管本地內存構建數組
四、性能測試

一、數組類型佈局

我們再簡單回顧一下數組對象的內存佈局。如下圖所示,對於32位(x86)系統,Object Header和TypeHandle各佔據4個字節;但是對於64位(x64)來說,存儲方法表指針的TypeHandle自然擴展到8個字節,但是Object Header依然是4個字節,爲了確保TypeHandle基於8字節的內存對齊,所以會前置4個字節的“留白(Padding)”。

image_thumb5_thumb

其荷載內容(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)的數組。

var array = BuildArray<string>(100);
Debug.Assert(array.Length == 100);
Debug.Assert(array.All(it => it is null));
for (int index = 0; index < array.Length; index++)
{
    array[index] = index.ToString();
}
for (int index = 0; index < array.Length; index++)
{
    Debug.Assert(array[index] == index.ToString());
}

三、利用非託管本地內存構建數組

既然我們可以利用一段連續的託管內存(字節數組)構建一個指定元素類型、指定長度的數組,我們自然也能利用非託管內存達到相同的目的。利用非託管本地內存構建數組帶來的最大好處顯而易見,那就是不會對GC造成任何壓力,前提是我們能夠自行釋放分配的內容。爲了我們將上面定義的BuildArray<T>方法改造成如下的形式:在完成針對字節數的計算之後,我們調用NativeMemory的AllocZeroed方法分配長度適合的內存,並將內容置空(設置爲零)。接下來按照佈局規則將TypeHandle和長度寫入對應的位置。最後讓返回的變量指向TypeHandle對應的地址就可以了。

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 pointer = NativeMemory.AllocZeroed((uint)byteCount);
    Unsafe.Write(Unsafe.Add<nint>(pointer, 1), typeof(T[]).TypeHandle.Value);
    Unsafe.Write(Unsafe.Add<nint>(pointer, 2), length);

    T[] array = null!;
    Unsafe.Write(Unsafe.AsPointer(ref array), new IntPtr(Unsafe.Add<nint>(pointer, 1)));
    return array;
}

unsafe static void Free<T>(T[] array)
{
    var address = *(nint*)Unsafe.AsPointer(ref array);
    NativeMemory.Free(Unsafe.Add<nint>(address.ToPointer(), -1));
}

上面的代碼還實現了用來釋放本地內存的Free方法。我們通過對指定數組變量進行“解地址”得到帶釋放數組對象的地址,但是這個地址並非分配內存的初始位置,所有我們需要前移一個身位(InPtr.Size)得到指向初始內存地址的指針,並將其作爲NativeMemory的Free方法的參數,這樣在BuildArray<T>方法中分配的內存就能被釋放了。

var random = new Random();
while (true)
{
    var length = random.Next(10, 100);
    var array = BuildArray<int>(length);
    Debug.Assert(array.Length == length);
    Debug.Assert(array.All(it=>it == 0));

    for (int index = 0; index < length; index++) array[index] = index;
    for (int index = 0; index<length; index++) Debug.Assert(array[index] == index);

    Free(array);
}

在如下的演示程序中,我們在一個無限循環中調用BuildArray<T>方法構建一個隨機長度的整型數組,然後我們利用調試斷言驗證其長度和元素初始值,然後對每個元素進行賦值並驗證。由於每次循環都調用Free方法對創建的數組對象進行了釋放,所以內存總是會維持在一個穩當的狀態,這可以從VS提供的針對內存的診斷工具得到驗證。

image

四、性能測試

我們最後做一個簡單的性能測試看看BuildArray<T> + Free<T>與直接new T[]這兩種編程方式的性能差異。如下面的代碼片段所示,我們定義了兩個Benchmark方法,ManagedArray方法直接返回利用new關鍵字創建的整型數組,長度爲1024;NativeArray方法調用BuildArray<T>方法構建了一個相同長度的整型數組,並調用Free方法將其“釋放”。

[MemoryDiagnoser]
public class Benchmark
{

    [Benchmark]
    public int[] ManagedArray()=> new int[1024];

    [Benchmark]
    public void NativeArray()=>Free(BuildArray<int>(1024));

    unsafe static T[] BuildArray<T>(int length);

    unsafe static void Free<T>(T[] array);
}

如下所示的是性能測試的結果,可以看出NativeArray不僅僅沒有基於GC的分配,耗時不到原來的一半。

image

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章