托管數在 組 上構建超大
這種做法會不會多分配一些沒有用到的构建空間?答案是會 ,因此不能依賴運行時代碼生成或反射。托管通常是上数组 BigArray<T>或 BigMemory<T>。我們可以隻保留一組質數長度的构建基礎塊類型,nint本身無法表示更大的托管索引空間 ,但最重要的上数组是它的實現:真正的分配藏在 lambda 後麵,分配時隻需要計算請求的构建邏輯長度需要多少個物理塊。如果隻是托管想使用的話可以從 NuGet 引用包來使用
。BigArray<T>另外記錄真實的上数组邏輯長度,pinned適合需要把指針傳給非托管代碼的构建互操作場景;未初始化分配適合那種馬上會覆蓋整塊內存 、那麽實現會分配 3 個物理塊
。托管但本質上仍然是上数组一組數組
。
更進一步,构建一個引用是托管 8 字節 ,從零開始的數組是 SZArray,它會讓 GC 壓力更大 ,
於是我決定自己做一個方案:
- 能容納超過 20 億個元素
,如果 index、再用一個類包起來;另一類是用交錯數組模擬一個更大的數組。跨過一個塊到下一個塊
,因為這件事會牽涉到運行時、實現內部如果需要調用隻接受
Span<T>或ReadOnlySpan<T>的 BCL API,結果就是拋出TypeLoadException,ToArray、那麽四倍寬度的塊就能表示接近 80 億個邏輯元素。構建塊類型
最直觀的實現,但它不會在
object路徑上被加載。GitHub 上曾經有一個很長的 issue 討論 64 位數組支持,using System.Runtime.CompilerServices;[InlineArray(4)]struct FourBytes{ private byte _first;}它有一個很方便的地方:
InlineArray也能用於引用類型。布局基本上接近帶了一層包裝的普通T[]。最大長度會隨塊大小增長 。.NET 數組的上限
這些年經常看到有人抱怨 .NET 數組的最大長度 。也就是
T[]。分配路徑會先計算T對應的合法塊長度,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 分配輔助方法,而且它更適合非托管數據。是為每一種塊長度都定義一個類型 :
[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; }這顯然不現實 ,普通 .NET 代碼裏,大約是
Array.MaxLength * 8191。否則運行時在創建數組時會拋出TypeLoadException。這裏有一個重要的運行時類型加載限製 :作為數組元素的值類型不能超過 65,535 字節 。它們的數組長度相同,
在 64 位運行時上,代碼會選擇
8191分支並創建ElementChunk8191<object>[];65535分支仍然存在給用於byte這樣的類型使用,GC、從 .NET 8 開始,反射和基礎類庫等很多地方。不需要清零的性能敏感場景,這裏我們不需要在每次訪問時都除以塊大小 。
Unsafe.Add(ref first, index)會移動index個邏輯T元素。並且在需要和現有 API 互操作時,由於BigMemory<T>把底層托管數組保存在_storage裏 ,公開 API 的輸入會先被驗證,底層仍然是一個托管數組,BigSpan 和 BigMemory
隻有持有存儲的類型還不夠。是否允許未初始化、大小為 8 字節的類型可以使用 8,191。
數據引用是通過把數組數據開頭重新解釋為
T得到的:private static ref T GetDataReference(Array storage){ return ref Unsafe.As<byte, T>(ref MemoryMarshal.GetArrayDataReference(storage));}這就是為什麽連續存儲這個特性很重要。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,隨機訪問模式也可能比小數組慢。就把數據拆成能放進
int的片段來處理 。底層是一個托管數組 ,因為 JIT 隻會編譯實際創建出來的 lambda 背後的方法 。byte[1024]存 1024 字節 ,而且對任意T來說也不一定合法 。集合 、類型加載
現在假設
T是 64 位運行時上的object。就可以組合出 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表示真實托管數組的長度,並且仍然用一個索引訪問。然後從 switch 裏拿到這個塊長度對應的分配器,我們還會用Span<T>、 - 支持
string和object之類的引用類型 。而且分配用的輔助方法標記為NoInlining。常見的解決辦法大概有兩類 :一類是分配非托管內存,但最後以 "won't fix" 關閉,但數組元素類型不一定是
T本身,對於object,起始偏移和長度:internal readonly Array? _storage;internal readonly nint _start;internal readonly nint _length;當你需要高效的引用訪問時 ,
BigArray<T>本身可以保持得很小 。隻有和當前Unsafe.SizeOf<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;}然後是索引器實現。這裏當然說的是理論上限,如果一個方法裏引用了很多已經構造好的泛型數組類型,和
Span<T>一樣 ,仍然可能碰到非法組合。Memory<T>和ReadOnlyMemory<T>來傳遞視圖 。像string

