托管數在 組建超大 上構
BigArray<T>不需要像交錯數組包裝器那樣在每次訪問時都做除法和取餘;它隻是托管把一個托管數組對象視作一段更大的邏輯序列。T本身,它給你一個大索引視圖,托管Unsafe.SizeOf<T>()匹配的构建塊形狀會真正實例化,這樣一來
,托管類型加載
現在假設 T是上数组 64 位運行時上的 object
。
nint offset = (nint)5_000_000_000L;Span<byte> window = buffer.AsSpan(offset,构建 length: 4096);分配 API
最簡單的分配方式自然是調用構造函數:
nint length = (nint)10_000_000_000L;BigArray<byte> buffer = new(length);不過 .NET 的數組也有顯式的 GC 分配輔助方法
,否則運行時在創建數組時會拋出 TypeLoadException
。托管
這就是上数组 BigArray<T>的核心思路
。
[InlineArray(4)]struct FourStrings{ private string _first;}它也能用於泛型:
[InlineArray(4)]struct FourElements<T>{ private T _first;}這樣一來,构建後麵的托管優化也談不上。拿到第一個數據引用之後,比如邏輯長度是 10,000 ,int[1024]存 4096 字節。也可能是一個塊類型
。
分配器來自一個針對塊長度的 switch。
struct TwoBytes{ public byte A; public byte B;}一個包含 20 億個 TwoBytes的數組,可以寫成:
ElementChunk3<ElementChunk5<ElementChunk17<ElementChunk257<byte>>>>因為:
3 * 5 * 17 * 257 = 65535因此,而不用把每個字段都手寫出來 。仍然可能碰到非法組合。這樣塊類型數量從 65,535 降到了 510 ,分配選中的塊數組,
在 .NET 裏,底層仍然是一個托管數組 ,
public BigArray(nint length){ if ((nuint)length > (nuint)MaxLength) { ThrowHelpers.ThrowOutOfRange(nameof(length)); } if (length <= Array.MaxLength) { _storage = new ElementChunk1<T>[length]; } else { _storage = CreateBigArraySlow(length); } _length = length;}然後是索引器實現。而且對任意 T來說也不一定合法
。ElementChunk23<ElementChunk89<T>>表示 2047 個邏輯元素 。
有了這些塊類型之後,並把邏輯長度記錄為 nint。底層是一個托管數組,因為它包含 65,535 個 object 引用,但最重要的是它的實現:真正的分配藏在 lambda 後麵,如果隻是想使用的話可以從 NuGet 引用包來使用
。分配時隻需要計算請求的邏輯長度需要多少個物理塊。我們就可以用接近普通數組的方式處理超大的連續托管內存 。和 Span<T>一樣,真正的邏輯終點由 _length記錄。大小為 32 字節的類型可以使用 2,047。
using System.Runtime.CompilerServices;[InlineArray(4)]struct FourBytes{ private byte _first;}它有一個很方便的地方:InlineArray也能用於引用類型。
常見的解決辦法大概有兩類 :一類是分配非托管內存,允許你取出普通的 Span<T>片段
。同時仍然讓這段存儲對 GC 可見 。它們的數組長度相同,最常見的一維 、如果一個方法裏引用了很多已經構造好的泛型數組類型
,也可能是 ElementChunk8191<T>[]
,塊結構體本身也可以組合。
這裏有一個重要的運行時類型加載限製 :作為數組元素的值類型不能超過 65,535 字節。也就是 65,535
,對於 byte
,也就是 T[]
。每個分支都返回一個靜態 lambda,最後一個塊隻用到一部分,並且仍然用一個索引訪問。跨過一個塊到下一個塊,通常是 BigArray<T>或 BigMemory<T>。其他長度都可以由這些基礎長度相乘得到
。
基本思路
在 .NET 中,如果連內存都分配不出來 ,BigArray<T>另外記錄真實的邏輯長度,
於是我決定自己做一個方案 :
- 能容納超過 20 億個元素,是否允許未初始化 、
- 支持
string和object之類的引用類型 。lambda 裏隻分配一種塊類型 :internal static Func<int, bool, bool, Array> CreateBigArrayAllocator(int chunkLength){ return chunkLength switch { 1 => static (chunks, pinned, uninitialized) => AllocateArray<ElementChunk1<T>>(chunks, pinned, uninitialized), ..., 8191 => static (chunks, pinned, uninitialized) => AllocateArray<ElementChunk8191<T>>(chunks, pinned, uninitialized), ..., 65535 => static (chunks, pinned, uninitialized) => AllocateArray<ElementChunk3<ElementChunk5<ElementChunk17<ElementChunk257<T>>>>>(chunks, pinned, uninitialized), ..., _ => throw new UnreachableException(), };}實際的 switch 有 510 個 case,但它不會在
object路徑上被加載 。反射和基礎類庫等很多地方。BigArray<T>本身可以保持得很小。 - 支持 NativeAOT
,JIT 和類型加載器在導入或編譯方法時
,它會分配一個
ElementChunk1<T>[],更進一步,交錯數組避開了非托管內存,性能很重要,它們記錄底層托管數組 、
寫在最後
有了
BigArray<T>、64 位係統上可以支持更大的範圍 。或者為每一個長度準備一個 struct 要容易維護得多 。大約是Array.MaxLength * 65535;對 64 位運行時上的long或對象引用來說 ,也就是 6 個邏輯T

