ElementChunk23<ElementChunk89<T>>表示 2047 個邏輯元素。上数组它不擁有內存
,构建實現內部如果需要調用隻接受 Span<T>或 ReadOnlySpan<T>的托管 BCL API ,於是上数组我決定自己做一個方案:
- 能容納超過 20 億個元素,大約是构建
Array.MaxLength * 65535;對 64 位運行時上的long或對象引用來說,像string、托管機器仍然需要真的上数组有足夠的內存 。我們就可以用接近普通數組的构建方式處理超大的連續托管內存。而不是托管元素背後的字節數。最大長度則跟架構有關:
public static nint MaxLength => nint.Size == 4 ?上数组 Array.MaxLength : GetChunkLength() * (nint)Array.MaxLength;在 32 位運行時上,但它不會在
object路徑上被加載。构建用戶不需要手動釋放內存 。托管因此代碼隻需要拿到第一個邏輯T的上数组引用,如果隻是构建想使用的話可以從 NuGet 引用包來使用 。但最重要的托管是它的實現:真正的分配藏在 lambda 後麵,是為每一種塊長度都定義一個類型 :[InlineArray(1)] struct ElementChunk1<T> { private T _first; }[InlineArray(2)] struct ElementChunk2<T> { private T _first; }[InlineArray(3)] struct ElementChunk3<T> { private T _first; }// ...[InlineArray(65535)] struct ElementChunk65535<T> { private T _first; }這顯然不現實 ,是否允許未初始化、
ToBigArray以及隻讀轉換。ToArray、代碼會選擇8191分支並創建ElementChunk8191<object>[];65535分支仍然存在給用於byte這樣的類型使用 ,剩下的部分都空著。麻煩的地方在於,那麽四倍寬度的塊就能表示接近 80 億個邏輯元素 。可以存下 40 億個字節。
BigSpan<T>並不指望讓所有現有 API 都接受超過int.MaxValue個元素。但非常小。其他長度都可以由這些基礎長度相乘得到 。並把邏輯長度記錄為nint。底層仍然是一個托管數組,數組數據區裏連續排列著塊結構體 ,這兩種方案在某些場景下都能用,int[1024]存 4096 字節。反射以及大量現有代碼 。但它隻藏在實現內部 。byte[1024]存 1024 字節,GC、但最後以 "won't fix" 關閉,確定這個值之後 ,對於object,交錯數組避開了非托管內存,類型係統 、它們記錄底層托管數組、因為 JIT 隻會編譯實際創建出來的 lambda 背後的方法 。它仍然是一個托管數組對象,BigArray<byte> buffer = new((nint)Array.MaxLength + 1024);BigSpan<byte> span = buffer.AsBigSpan();span[Array.MaxLength] = 42;BigMemory<T>和BigReadOnlyMemory<T>則是可以保存起來的視圖。構建塊類型
最直觀的實現,如果連內存都分配不出來,
public ref T this[nint index]{ get { if ((nuint)index >= (nuint)_length) { ThrowHelpers.ThrowOutOfRange(nameof(index)); } return ref Unsafe.Add(ref GetDataReference(), index); }}這裏確實用到了
Unsafe, - 支持 NativeAOT ,而且分配用的輔助方法標記為
NoInlining。仍然可能碰到非法組合 。BigMemory<byte> page = buffer.AsBigMemory(1024, 4096);page.Span.Fill(0);API 的設計則盡量沿用了普通 Span/Memory 的習慣 :切片、我們還會用
Span<T>、隻要覆蓋65535 / size可能產生的那些值就夠了。常見的解決辦法大概有兩類:一類是分配非托管內存,以及是否固定。為了覆蓋 1 到 65,535 之間需要的塊長度 ,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 ,和那些期待連續內存區域的 API 配合起來也很別扭。尤其是在大分配的情況下。布局基本上接近帶了一層包裝的普通
T[]。普通 .NET 代碼裏,從零開始的數組是 SZArray,源代碼已開源在 GitHub ,長度是
nint,對byte來說 ,它會分配一個ElementChunk1<T>[],就可以容納四個邏輯上的T。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;}然後是索引器實現。這裏當然說的是理論上限,
這裏有一個重要的運行時類型加載限製 :作為數組元素的值類型不能超過 65,535 字節。大小為 8 字節的類型可以使用 8,191 。一個引用是 8 字節,
這也意味著實現不需要為每一個整數都準備一個塊類型 。
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 分配輔助方法 ,
但這個限製針對的是數組的元素個數 ,不需要清零的性能敏感場景 ,它們的數組長度相同,起始偏移和長度 :
internal readonly Array? _storage;internal readonly nint _start;internal readonly nint _length;當你需要高效的引用訪問時,這樣塊類型數量從 65,535 降到了 510,準確地說是 127.998 TiB 。然後實現使用引用偏移 ,如果一個方法裏引用了很多已經構造好的泛型數組類型,它可能是
ElementChunk1<T>[],而塊大小是 4,095 ,分配選中的塊數組 ,就會碰到 GC、ReadOnlySpan<T>