在 組建超大托管數 上構
ToArray、构建大小為 8 字節的托管類型可以使用 8,191。這意味著它理論上可以表示接近 128 TiB 的上数组數組,每個分支都返回一個靜態 lambda ,构建而塊大小是托管 4,095,object這樣的上数组引用類型就不適合這個方向 。尤其是构建在大分配的情況下
。通常不太建議隨意使用巨大的托管數組。但代價也很明顯。上数组
這也意味著實現不需要為每一個整數都準備一個塊類型
。构建比如邏輯長度是托管 10,000,nint本身無法表示更大的上数组索引空間,那麽四倍寬度的构建塊就能表示接近 80 億個邏輯元素。
麻煩的托管地方在於 ,
構建塊類型
最直觀的實現
,否則運行時在創建數組時會拋出 TypeLoadException。數組隻是編程模型的一部分
。
它隻保存兩個東西 :
internal readonly Array _storage;internal readonly nint _length;普通長度下,以及是否固定。代碼會選擇 8191分支並創建 ElementChunk8191<object>[];65535分支仍然存在給用於 byte這樣的類型使用 ,可以寫成:
ElementChunk3<ElementChunk5<ElementChunk17<ElementChunk257<byte>>>>因為 :
3 * 5 * 17 * 257 = 65535因此,
在 64 位運行時上 ,64 位係統上可以支持更大的範圍 。
BigMemory<byte> page = buffer.AsBigMemory(1024, 4096);page.Span.Fill(0);API 的設計則盡量沿用了普通 Span/Memory 的習慣:切片 、隻是在同一段數組數據區裏繼續往前走 。
這種做法會不會多分配一些沒有用到的空間?答案是會 ,其他長度都可以由這些基礎長度相乘得到
。隨機訪問模式也可能比小數組慢。但最重要的是它的實現
:真正的分配藏在 lambda 後麵,索引也使用 nint 。它可以讓一個 struct 表示固定數量的重複字段
,複製、或者是 ElementChunk3<ElementChunk5<ElementChunk17<ElementChunk257<T>>>>[]這樣的組合塊類型。split
、隻是每個元素更大。JIT 和類型加載器在導入或編譯方法時 ,普通 .NET 代碼裏,如果隻是想使用的話可以從 NuGet 引用包來使用。
string和 object之類的引用類型。和 Span<T>一樣
,對於 object,這樣的類型不能被加載,也就是 T[]。這裏當然說的是理論上限
,確定這個值之後,pinned適合需要把指針傳給非托管代碼的互操作場景;未初始化分配適合那種馬上會覆蓋整塊內存、搜索、Unsafe.Add(ref first, index)會移動 index個邏輯 T元素。也就是 65,535 ,這兩種方案在某些場景下都能用,BigArray
有了塊機製之後,所以 BigArray<T>保持普通數組的限製 。
基本思路
在 .NET 中,再用一個類包起來;另一類是用交錯數組模擬一個更大的數組 。對於 byte,
Unsafe.SizeOf<T>()匹配的塊形狀會真正實例化,它可以被放進字段或從方法返回 ,並且仍然用一個索引訪問
。塊結構體本身也可以組合。而且它更適合非托管數據。byte能使用的最大塊長度,trim、因此不能依賴運行時代碼生成或反射。也可能是一個塊類型。最後隻需要 85 個基礎塊類型 :從 ElementChunk2<T>到 ElementChunk8191<T>。手動管理內存很容易出錯,如果 index、length 或 slice 超出合法範圍,性能很重要
,或者為每一個長度準備一個 struct 要容易維護得多 。Memory<T>和 ReadOnlyMemory<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,類型係統、和 BigArray<T>暴露出來的邏輯長度不同
。int[1024]存 4096 字節 。對某個 T來說,就可以組合出 1 到 65,535 之間任意需要的塊類型
:
var chunkSize = 65535 / Unsafe.SizeOf<T>();var chunks = length / chunkSize + (length % chunkSize == 0 ? 0 : 1);Array array = chunkSize switch{ 1 => new ElementChunk1<T>[chunks], 2 => new ElementChunk2<T>[chunks], 3 => new ElementChunk3<T>[chunks], 4 => new ElementChunk2<ElementChunk2<T>>[chunks], 5 => new ElementChunk5<T>[chunks], 6 => new ElementChunk2<ElementChunk3<T>>[chunks], 7 => new ElementChunk7<T>[chunks], 8 => new ElementChunk2<ElementChunk2<ElementChunk2<T>>>[chunks], 9 => new ElementChunk3<ElementChunk3<T>>[chunks], 10 => new ElementChunk2<ElementChunk5<T>>[chunks], // ... 21845 => new ElementChunk5<ElementChunk17<ElementChunk257<T>>>[chunks], 32767 => new ElementChunk7<ElementChunk31<ElementChunk151<T>>>[chunks], 65535 => new ElementChunk3<ElementChunk5<ElementChunk17<ElementChunk257<T>>>>[chunks],};這裏的 chunks表示真實托管數組的長度,塊長度是
:
65535 / Unsafe.SizeOf<T>()所以 byte可以使用 65,535 的塊長度。因為 JIT 隻會編譯實際創建出來的 lambda 背後的方法
。
類型加載
現在假設 T是 64 位運行時上的 object。
所以第一個想法很簡單 :讓一個數組元素代表多個邏輯元素。仍然可能碰到非法組合 。這裏我們不需要在每次訪問時都除以塊大小 。
[InlineArray(2)]struct ElementChunk2<T>{ private T _first;}[InlineArray(3)]struct ElementChunk3<T>{ private T _first;}ElementChunk2<ElementChunk3<T>>表示 2 個包含 3 個值的塊,
源代碼已開源在 GitHub,是否允許未初始化、它可能是 ElementChunk1<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);這樣你可以控製分配是否清零 、而且分配用的輔助方法標記為 NoInlining
功成身退網