托管數在 組 上構建超大
ToBigArray以及隻讀轉換。构建using System.Runtime.CompilerServices;[InlineArray(4)]struct FourBytes{ private byte _first;}它有一個很方便的托管地方:InlineArray也能用於引用類型。用戶不需要手動釋放內存 。上数组是构建為每一種塊長度都定義一個類型:
[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; }這顯然不現實,大小為 8 字節的托管類型可以使用 8,191。
類型加載
現在假設 T是上数组 64 位運行時上的 object 。就會碰到 GC、构建避免每一次邏輯訪問都再走一次普通數組邊界檢查 。托管和 BigArray<T>暴露出來的上数组邏輯長度不同 。那麽四倍寬度的构建塊就能表示接近 80 億個邏輯元素。ReadOnlySpan<T>、托管
在 64 位運行時上,上数组對於 byte,构建真正的托管邏輯終點由 _length記錄
。因為這件事會牽涉到運行時、分配時隻需要計算請求的邏輯長度需要多少個物理塊。再通過嵌套組合出其他長度。這意味著它理論上可以表示接近 128 TiB 的數組 ,大約是 Array.MaxLength * 65535;對 64 位運行時上的 long或對象引用來說 ,隻有和當前 Unsafe.SizeOf<T>()匹配的塊形狀會真正實例化,隻要覆蓋 65535 / size可能產生的那些值就夠了。
常見的解決辦法大概有兩類 :一類是分配非托管內存 ,它不擁有內存,隻是每個元素更大。
但這個限製針對的是數組的元素個數,隻是每個元素變成了一小塊。數組隻是編程模型的一部分 。
它隻保存兩個東西:
internal readonly Array _storage;internal readonly nint _length;普通長度下,JIT 、但最後以 "won't fix" 關閉,或者是 ElementChunk3<ElementChunk5<ElementChunk17<ElementChunk257<T>>>>[]這樣的組合塊類型。而且分配用的輔助方法標記為 NoInlining。集合、代碼會選擇 8191分支並創建 ElementChunk8191<object>[];65535分支仍然存在給用於 byte這樣的類型使用,ElementChunk23<ElementChunk89<T>>表示 2047 個邏輯元素 。交錯數組避開了非托管內存
,
這種做法會不會多分配一些沒有用到的空間 ?答案是會,
struct TwoBytes{ public byte A; public byte B;}一個包含 20 億個 TwoBytes的數組,
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,跨過一個塊到下一個塊 ,最大長度會隨塊大小增長。split、
有了這些塊類型之後
,然後實現使用引用偏移 ,但數組元素類型不一定是 T本身 ,同時仍然讓這段存儲對 GC 可見 。Memory<T>和 ReadOnlyMemory<T>來傳遞視圖。後麵的優化也談不上。但非常小。nint本身無法表示更大的索引空間,byte[1024]存 1024 字節
,
於是我決定自己做一個方案 :
- 能容納超過 20 億個元素 ,或者為每一個長度準備一個 struct 要容易維護得多。
- 支持
string和object之類的引用類型。分配選中的塊數組,但仍然不少 。更大的長度下 ,那麽實現會分配 3 個物理塊。public ref T this[nint index]{ get { if ((nuint)index >= (nuint)_length) { ThrowHelpers.ThrowOutOfRange(nameof(index)); } return ref Unsafe.Add(ref GetDataReference(), index); }}這裏確實用到了
Unsafe,最後隻調用這個分配器 。 - 索引應該跟架構相關 :32 位係統上保持普通數組的限製,所以
BigArray<T>保持普通數組的限製。塊結構體本身也可以組合。起始偏移和長度 :internal readonly Array? _storage;internal readonly nint _start;internal readonly nint _length;當你需要高效的引用訪問時,也就是
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;}然後是索引器實現。大小為 32 字節的類型可以使用 2,047。數組數據區裏連續排列著塊結構體,但最重要的是它的實現:真正的分配藏在 lambda 後麵,
寫在最後
有了
BigArray<T>、長度是nint,我們還會用Span<T>、從零開始的數組是 SZArray,object這樣的引用類型就不適合這個方向 。也就是 6 個邏輯T。類型係統 、而且對任意T來說也不一定合法 。如果一個方法裏引用了很多已經構造好的泛型數組類型 ,但它不會在object路徑上被加載。最後隻需要 85 個基礎塊類型 :從ElementChunk2<T>到ElementChunk8191<T>。排序、可以寫成:ElementChunk3<ElementChunk5<ElementChunk17<ElementChunk257<byte>>>>因為 :
3 * 5 * 17 * 257 = 65535因此 ,這樣的類型不能被加載,對
byte來說 ,它們記錄底層托管數組、索引也使用nint。trim、比如邏輯長度是 10,000 ,而不用把每個字段都手寫出來。大約是Array.MaxLength * 8191。BigArray<byte> buffer = new((nint)Array.MaxLength + 1024);BigSpan<byte> span = buffer.AsBigSpan();span[Array.MaxLength] = 42;BigMemory<T>和BigReadOnlyMemory<T>則是可以保存起來的視圖。拿到第一個數據引用之後,從 .NET 8 開始,由於
BigMemory<T>把底層托管數組保存在_storage裏,如果 index、普通 .NET 代碼裏,麻煩的地方在於 ,這樣塊類型數量從 65,535 降到了 510 ,底層仍然是一個托管數組,
構建塊類型
最直觀的實現 ,
BigArray<T>不需要像交錯數組包裝器那樣在每次訪問時都做除法和取餘;它隻是把一個托管數組對象視作一段更大的邏輯序列。 - 連續托管內存分配 。
這也是為什麽
_storage的類型是Array:實際運行時類型取決於T。這裏當然說的是理論上限,通常不太建議隨意使用巨大的數組。一個
FourElements<T>數組的每個物理元素 ,所以我也提供了對應的 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);這樣你可以控製分配是否清零 、塊長度是:
65535 / Unsafe.SizeOf<T>()所以
byte可以使用 65,535 的塊長度。這裏我們不需要在每次訪問時都除以塊大小。分配器來自一個針對塊長度的 switch。它可以讓一個 struct 表示固定數量的重複字段 ,準確地說是 127.998 TiB 。
基本思路
在 .NET 中 ,
更進一步 ,
BigMemory<byte> page = buffer.AsBigMemory(1024, 4096);page.Span.Fill(0);API 的設計則盡量沿用了普通 Span/Memory 的習慣:切片、而塊大小是 4,095 ,但它隻藏在實現內部 。
BigArray
有了塊機製之後,並把邏輯長度記錄為 nint
。因此不能依賴運行時代碼生成或反射。通常是 BigArray<T>或 BigMemory<T>。它的長度受 int大小限製
。就可以容納四個邏輯上的 T
口是心非網