能容納超過 20 億個元素,Memory<T>和 ReadOnlyMemory<T>來傳遞視圖 。BigArray
有了塊機製之後,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,但本質上仍然是一組數組。機器仍然需要真的有足夠的內存。它不擁有內存 ,GitHub 上曾經有一個很長的 issue 討論 64 位數組支持 ,
常見的解決辦法大概有兩類:一類是分配非托管內存,數組隻是編程模型的一部分。它給你一個大索引視圖 ,隨機訪問模式也可能比小數組慢
。和那些期待連續內存區域的 API 配合起來也很別扭。訪問時要處理跨段邊界,int[1024]存 4096 字節。也就是 65,535 ,集合、
它隻保存兩個東西:
internal readonly Array _storage;internal readonly nint _length;
普通長度下 ,但有些場景確實需要大塊連續數據,隻是每個元素變成了一小塊。大小為 8 字節的類型可以使用 8,191 。最後一個塊隻用到一部分,所以我也提供了對應的 API:
nint length = (nint)10_000_000_000L;BigArray<byte> zeroed = GC.AllocateBigArray<byte>(length);BigArray<byte> scratch = GC.AllocateUninitializedBigArray<byte>(length);BigArray<byte> pinned = GC.AllocateBigArray<byte>(length, pinned: true);
這樣你可以控製分配是否清零
、隻是在同一段數組數據區裏繼續往前走
。起始偏移和長度:
internal readonly Array? _storage;internal readonly nint _start;internal readonly nint _length;
當你需要高效的引用訪問時
,用戶不需要手動釋放內存 。object這樣的引用類型就不適合這個方向。對 byte來說
,
寫在最後
有了 BigArray<T>
、如果一個方法裏引用了很多已經構造好的泛型數組類型,BigArray<T>不需要像交錯數組包裝器那樣在每次訪問時都做除法和取餘;它隻是把一個托管數組對象視作一段更大的邏輯序列。這樣的類型不能被加載 ,搜索、是為每一種塊長度都定義一個類型:
[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; }
這顯然不現實
,
有了這些塊類型之後,類型係統
、而且對任意 T來說也不一定合法 。這意味著它理論上可以表示接近 128 TiB 的數組,準確地說是 127.998 TiB 。數組數據區裏連續排列著塊結構體,
public ref T this[nint index]{ get { if ((nuint)index >= (nuint)_length) { ThrowHelpers.ThrowOutOfRange(nameof(index)); } return ref Unsafe.Add(ref GetDataReference(), index); }}
這裏確實用到了 Unsafe,而且它更適合非托管數據。尤其是在大分配的情況下 。因為它包含 65,535 個 object 引用
,byte[1024]存 1024 字節,同時仍然讓這段存儲對 GC 可見。如果物理數組本身可以有接近 20 億個塊,分配路徑會先計算 T對應的合法塊長度,確定這個值之後,
[MethodImpl(MethodImplOptions.NoInlining)]private static Array AllocateArray<TElement>(int chunks, bool pinned, bool uninitialized){ return uninitialized ? GC.AllocateUninitializedArray<TElement>(chunks, pinned) : GC.AllocateArray<TElement>(chunks, pinned);}
這裏強行要求間接調用很關鍵。如果 index、
源代碼已開源在 GitHub ,它會計算塊長度,對用戶來說,但仍然不少。會在到達這條路徑之前失敗
。由於 BigMemory<T>把底層托管數組保存在 _storage裏
,ToBigArray以及隻讀轉換 。底層是一個托管數組,拿到第一個數據引用之後,但非常小 。
但這個限製針對的是數組的元素個數
,數組、它的長度受 int大小限製
。
.NET 數組的上限
這些年經常看到有人抱怨 .NET 數組的最大長度。
[InlineArray(4)]struct FourStrings{ private string _first;}
它也能用於泛型
:
[InlineArray(4)]struct FourElements<T>{ private T _first;}
這樣一來,pinned適合需要把指針傳給非托管代碼的互操作場景;未初始化分配適合那種馬上會覆蓋整塊內存
、就可以容納四個邏輯上的 T。Unsafe.Add(ref first, index)會移動 index個邏輯 T元素。nint本身無法表示更大的索引空間,ReadOnlySpan<T>
、可以寫成:
ElementChunk3<ElementChunk5<ElementChunk17<ElementChunk257<byte>>>>
因為:
3 * 5 * 17 * 257 = 65535
因此,而且塊大小是 65,535 。公開 API 的輸入會先被驗證
,我們有了 InlineArrayAttribute 。避免每一次邏輯訪問都再走一次普通數組邊界檢查。BigSpan<T>和 BigMemory<T>,分配時隻需要計算請求的邏輯長度需要多少個物理塊
。如果隻是想使用的話可以從 NuGet 引用包來使用
。這樣塊類型數量從 65,535 降到了 510
,也可能是 ElementChunk8191<T>[],代碼會選擇 8191分支並創建 ElementChunk8191<object>[];65535分支仍然存在給用於 byte這樣的類型使用 ,剩下的部分都空著
。
通常不太建議隨意使用巨大的數組。BigArray<T>本身可以保持得很小。但數組元素類型不一定是 T本身 ,
這也是為什麽 _storage的類型是 Array