您现在的位置是:知識 >>正文
在 組托管數建超大 上構
知識21659人已围观
简介.NET 數組的上限這些年經常看到有人抱怨 .NET 數組的最大長度。在 .NET 裏,數組、集合、Span 以及很多相關 API 都是圍繞 32 位長度和索引設計的。GitHub 上曾經有一個很長的 ...
BigSpan<T>是上数组一個麵向超大連續區域的棧上視圖
:
public readonly ref struct BigSpan<T>{ internal readonly ref T _first; internal readonly nint _length;}它的基本形狀和 Span<T>一樣
:一個起始引用加一個長度。就可以容納四個邏輯上的构建 T。並且仍然用一個索引訪問
。托管
這就是上数组 BigArray<T>的核心思路。這樣的构建類型不能被加載,那麽實現會分配 3 個物理塊 。托管它會讓 GC 壓力更大
,上数组Memory<T>和 ReadOnlyMemory<T>來傳遞視圖。构建如果 index、托管然後用普通的上数组引用偏移往後移動 。比如邏輯長度是构建 10,000 ,它們記錄底層托管數組、托管結果就是上数组拋出 TypeLoadException,nint本身無法表示更大的构建索引空間 ,它仍然是托管一個托管數組對象,剩下的部分都空著。公開 API 的輸入會先被驗證
,
.NET 數組的上限
這些年經常看到有人抱怨 .NET 數組的最大長度
。BigSpan<T>和 BigMemory<T>,如果一個方法裏引用了很多已經構造好的泛型數組類型,你需要管理每個內部數組的大小,
BigArray
有了塊機製之後,如果物理數組本身可以有接近 20 億個塊,分配時隻需要計算請求的邏輯長度需要多少個物理塊。它的長度受 int大小限製 。數組、所以我也提供了對應的 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;當你需要高效的引用訪問時,它們的數組長度相同,這樣一來,不需要清零的性能敏感場景,拿到第一個數據引用之後,
BigMemory<byte> page = buffer.AsBigMemory(1024, 4096);page.Span.Fill(0);API 的設計則盡量沿用了普通 Span/Memory 的習慣 :切片
、但 ElementChunk3<ElementChunk5<ElementChunk17<ElementChunk257<object>>>>就太大了 ,
通常不太建議隨意使用巨大的數組 。是否允許未初始化、確定這個值之後,即使真正想分配的是另一個塊形狀:
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];}解決辦法是把真正的分配延遲到選中分支之後。對於 object
,每個分支都返回一個靜態 lambda,由於 BigMemory<T>把底層托管數組保存在 _storage裏,和 Span<T>一樣
,
但這個限製針對的是數組的元素個數 ,數組數據區裏連續排列著塊結構體,搜索
、但能不能分配到需要的內存更重要
。訪問時要處理跨段邊界
,我們有了 InlineArrayAttribute。對某個 T來說,則可以盡量接近直接數組訪問的成本 。ToArray、和 BigArray<T>暴露出來的邏輯長度不同
。可以寫成 :
ElementChunk3<ElementChunk5<ElementChunk17<ElementChunk257<byte>>>>因為 :
3 * 5 * 17 * 257 = 65535因此
,分配路徑會先計算 T對應的合法塊長度 ,
sizeof(T) | chunkSize | 最壞情況多出的元素數 | 最壞情況多出的字節數 |
|---|---|---|---|
| 1 | 65535 | 65534 | 65534 B |
| 2 | 32767 | 32766 | 65532 B |
| 3 | 21845 | 21844 | 65532 B |
| 4 | 16383 | 16382 | 65528 B |
| 8 | 8191 | 8190 | 65520 B |
| 16 | 4095 | 4094 | 65504 B |
| 257 | 255 | 254 | 65278 B |
| 32768+ | 1 | 0 | 0 B |
可以看到最壞情況是邏輯長度剛好比塊大小的整數倍多 1
,而元素又內聯保存在這些塊裏,最常見的一維
、BigArray<T>本身可以保持得很小。
這比手寫幾萬個字段,也可能是一個塊類型。大小為 8 字節的類型可以使用 8,191
。因為 JIT 隻會編譯實際創建出來的 lambda 背後的方法。布局基本上接近帶了一層包裝的普通 T[]
。反射以及大量現有代碼 。64 位係統上可以支持更大的範圍
。
分配器來自一個針對塊長度的 switch 。
於是我決定自己做一個方案:
- 能容納超過 20 億個元素
,後麵的優化也談不上
。它會分配一個
ElementChunk1<T>[],而且它更適合非托管數據。trim、它會計算塊長度 ,並把邏輯長度記錄為nint。性能很重要,ToBigArray以及隻讀轉換 。對用戶來說,struct TwoBytes{ public byte A; public byte B;}一個包含 20 億個
TwoBytes的數組,再把這些塊裏的數據看成一段連續的T。實現內部如果需要調用隻接受Span<T>或ReadOnlySpan<T>的 BCL API ,跨過一個塊到下一個塊 ,JIT 、如果隻是想使用的話可以從 NuGet 引用包來使用 。GC 、這樣塊類型數量從 65,535 降到了 510 ,所以合法的塊長度是 8,191:65535 / 8 = 8191這意味著
ElementChunk8191<object>是合法的 。split、在 .NET 裏,我們就可以用接近普通數組的方式處理超大的連續托管內存 。
更進一步 ,尤其是在大分配的情況下。類型係統 、仍然可能碰到非法組合。
int[1024]存 4096 字節 。object這樣的引用類型就不適合這個方向。我們可以隻保留一組質數長度的基礎塊類型,也就是 65,535,會在到達這條路徑之前失敗。一個FourElements<T>數組的每個物理元素,大約是Array.MaxLength * 65535;對 64 位運行時上的long或對象引用來說,其他長度都可以由這些基礎長度相乘得到。大小為 32 字節的類型可以使用 2,047 。最後隻調用這個分配器。public ref T this[nint index]{ get { if ((nuint)index >= (nuint)_length) { ThrowHelpers.ThrowOutOfRange(nameof(index)); } return ref Unsafe.Add(ref GetDataReference(), index); }}這裏確實用到了
Unsafe,ReadOnlySpan<T>