Span<T>、托管再把這些塊裏的上数组數據看成一段連續的 T
。類型係統 、构建所以合法的托管塊長度是 8,191:65535 / 8 = 8191這意味著 ElementChunk8191<object>是合法的。反射和基礎類庫等很多地方。上数组最後隻需要 85 個基礎塊類型:從 ElementChunk2<T>到 ElementChunk8191<T>。构建即使真正想分配的托管是另一個塊形狀:
AllocateArray<object>(42); // TypeLoadException: Array of type 'ElementChunk3`1[ElementChunk5`1[ElementChunk17`1[ElementChunk257`1[System.__Canon]]]]' from assembly 'ConsoleApp1' cannot be created because base value type is too large.Array AllocateArray<T>(int length){ if (length <= 8191) return new ElementChunk8191<T>[length]; else return new ElementChunk3<ElementChunk5<ElementChunk17<ElementChunk257<T>>>>[length];}解決辦法是把真正的分配延遲到選中分支之後 。然後用普通的上数组引用偏移往後移動 。
這種做法會不會多分配一些沒有用到的构建空間 ?答案是會 ,
數據引用是托管通過把數組數據開頭重新解釋為 T得到的:
private static ref T GetDataReference(Array storage){ return ref Unsafe.As<byte, T>(ref MemoryMarshal.GetArrayDataReference(storage));}這就是為什麽連續存儲這個特性很重要。然後從 switch 裏拿到這個塊長度對應的上数组分配器,並把邏輯長度記錄為 nint。构建而不用把每個字段都手寫出來
。托管pinned適合需要把指針傳給非托管代碼的互操作場景;未初始化分配適合那種馬上會覆蓋整塊內存、這裏當然說的是理論上限,
源代碼已開源在 GitHub,複製、並且在需要和現有 API 互操作時,實現內部如果需要調用隻接受 Span<T>或 ReadOnlySpan<T>的 BCL API,跨過一個塊到下一個塊
,也可能是 ElementChunk8191<T>[],隨機訪問模式也可能比小數組慢。長度是 nint,
分配器來自一個針對塊長度的 switch 。從零開始的數組是 SZArray ,剩下的部分都空著。以及是否固定
。我們有了 InlineArrayAttribute 。就可以組合出 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表示真實托管數組的長度
,它的長度受 int大小限製。BigSpan<T>和 BigMemory<T>,
string和 object之類的引用類型
。隻是在同一段數組數據區裏繼續往前走。JIT 、BigMemory<byte> page = buffer.AsBigMemory(1024, 4096);page.Span.Fill(0);API 的設計則盡量沿用了普通 Span/Memory 的習慣:切片、Memory<T>和 ReadOnlyMemory<T>來傳遞視圖 。而不是元素背後的字節數 。性能很重要,則可以盡量接近直接數組訪問的成本。如果 index、拿到第一個數據引用之後,
這比手寫幾萬個字段,object這樣的引用類型就不適合這個方向。交錯數組避開了非托管內存,隻是每個元素變成了一小塊。或者是 ElementChunk3<ElementChunk5<ElementChunk17<ElementChunk257<T>>>>[]這樣的組合塊類型。是否允許未初始化 、但能不能分配到需要的內存更重要。
於是我決定自己做一個方案 :
- 能容納超過 20 億個元素 ,GitHub 上曾經有一個很長的 issue 討論 64 位數組支持,
BigSpan<T>是一個麵向超大連續區域的棧上視圖:public readonly ref struct BigSpan<T>{ internal readonly ref T _first; internal readonly nint _length;}它的基本形狀和
Span<T>一樣:一個起始引用加一個長度 。手動管理內存很容易出錯 ,但有些場景確實需要大塊連續數據,
using System.Runtime.CompilerServices;[InlineArray(4)]struct FourBytes{ private byte _first;}它有一個很方便的地方:
InlineArray也能用於引用類型 。那麽四倍寬度的塊就能表示接近 80 億個邏輯元素。但ElementChunk3<ElementChunk5<ElementChunk17<ElementChunk257<object>>>>就太大了,而且對任意T來說也不一定合法 。再用一個類包起來;另一類是用交錯數組模擬一個更大的數組。我們就可以用接近普通數組的方式處理超大的連續托管內存。trim、用戶不需要手動釋放內存。它給你一個大索引視圖 ,這就是
BigArray<T>的核心思路 。反射以及大量現有代碼。BigArray<T>另外記錄真實的邏輯長度 ,但它隻藏在實現內部。想要直接放寬這個限製,byte[1024]存 1024 字節,結果就是拋出TypeLoadException,也就是T[]。ElementChunk23<ElementChunk89<T>>表示 2047 個邏輯元素 。一個FourElements<T>數組的每個物理元素,Unsafe.Add(ref first, index)會移動index個邏輯T元素 。構建塊類型
最直觀的實現,你需要管理每個內部數組的大小,對於
object,這樣塊類型數量從 65,535 降到了 510 ,這裏有一個重要的運行時類型加載限製:作為數組元素的值類型不能超過 65,535 字節 。
.NET 數組的上限
這些年經常看到有人抱怨 .NET 數組的最大長度。對用戶來說,集合、
通常不太建議隨意使用巨大的數組。同時仍然讓這段存儲對 GC 可見。
ToArray、但這個限製針對的是數組的元素個數,其他長度都可以由這些基礎長度相乘得到 。最後一個塊隻用到一部分 ,然後實現使用引用偏移,由於
BigMemory<T>把底層托管數組保存在_storage裏,是為每一種塊長度都定義一個類型 :[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; }這顯然不現實 ,
BigArray<byte> buffer = new((nint)Array.MaxLength + 1024);BigSpan<byte> span = buffer.AsBigSpan();span[Array.MaxLength] = 42;BigMemory<T>和BigReadOnlyMemory<T>則是可以保存起來的視圖 。它可以防止未選中的塊數組類型被提前加載 。這樣一來,BigArray
有了塊機製之後,再通過嵌套組合出其他長度。而且塊大小是 65,535。但非常小。
這也是為什麽
_storage的類型是Array:實際運行時類型取決於T。GC 、因為它包含 65,535 個 object 引用 ,對某個T來說,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 ,
[InlineArray(4)]struct FourStrings{ private string _first;}它也能用於泛型:
[InlineArray(4)]struct FourElements<T>{ private T _first;}這樣一來,它們的
Span屬性會生成BigSpan<T>或BigReadOnlySpan<T>