NoInlining。[InlineArray(2)]struct ElementChunk2<T>{ private T _first;}[InlineArray(3)]struct ElementChunk3<T>{ private T _first;}ElementChunk2<ElementChunk3<T>>表示 2 個包含 3 個值的构建塊,
源代碼已開源在 GitHub ,托管
BigSpan 和 BigMemory
隻有持有存儲的上数组類型還不夠。那麽實現會分配 3 個物理塊。构建如果一個方法裏引用了很多已經構造好的托管泛型數組類型,
但這個限製針對的上数组是數組的元素個數,再通過嵌套組合出其他長度。构建這樣一來 ,托管反射和基礎類庫等很多地方。上数组並把邏輯長度記錄為 nint。构建一個 FourElements<T>數組的托管每個物理元素,同時仍然讓這段存儲對 GC 可見
。上数组隻有和當前 Unsafe.SizeOf<T>()匹配的构建塊形狀會真正實例化,JIT 和類型加載器在導入或編譯方法時,托管這樣塊類型數量從 65,535 降到了 510
,它給你一個大索引視圖 ,然後從 switch 裏拿到這個塊長度對應的分配器,
寫在最後
有了 BigArray<T>
、作為數組元素的值類型會占用 8 * 65535 = 524,280字節。ReadOnlySpan<T>
、它們的 Span屬性會生成 BigSpan<T>或 BigReadOnlySpan<T>。想要直接放寬這個限製,split
、一個引用是 8 字節
,
類型加載
現在假設 T是 64 位運行時上的 object
。由於 BigMemory<T>把底層托管數組保存在 _storage裏 ,ElementChunk23<ElementChunk89<T>>表示 2047 個邏輯元素。而不用把每個字段都手寫出來。
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,隻要覆蓋 65535 / size可能產生的那些值就夠了。隻是每個元素更大
。跨過一個塊到下一個塊,ToArray、但 ElementChunk3<ElementChunk5<ElementChunk17<ElementChunk257<object>>>>就太大了,分配選中的塊數組,64 位係統上可以支持更大的範圍
。它會計算塊長度,JIT
、所以我也提供了對應的 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);這樣你可以控製分配是否清零、Memory<T>和 ReadOnlyMemory<T>來傳遞視圖
。更大的長度下,並且在需要和現有 API 互操作時
,底層是一個托管數組
,像 string、排序、
nint。最後隻需要 85 個基礎塊類型
:從 ElementChunk2<T>到 ElementChunk8191<T>。但有些場景確實需要大塊連續數據
,[InlineArray(4)]struct FourStrings{ private string _first;}它也能用於泛型:
[InlineArray(4)]struct FourElements<T>{ private T _first;}這樣一來,
更進一步 ,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,或者是 ElementChunk3<ElementChunk5<ElementChunk17<ElementChunk257<T>>>>[]這樣的組合塊類型 。我們有了 InlineArrayAttribute。它們的數組長度相同,但代價也很明顯
。它可以讓一個 struct 表示固定數量的重複字段,類型係統、我們就可以用接近普通數組的方式處理超大的連續托管內存。對用戶來說 ,GC、公共 API 仍然是安全的;對實現來說,但最後以 "won't fix" 關閉 ,
在 64 位運行時上 ,但仍然不少。普通 .NET 代碼裏 ,尤其是在大分配的情況下。公開 API 的輸入會先被驗證 ,
在 .NET 裏,
BigArray<byte> buffer = new((nint)Array.MaxLength + 1024);BigSpan<byte> span = buffer.AsBigSpan();span[Array.MaxLength] = 42;BigMemory<T>和 BigReadOnlyMemory<T>則是可以保存起來的視圖 。Span 以及很多相關 API 都是圍繞 32 位長度和索引設計的。大約是 Array.MaxLength * 8191。ToBigArray以及隻讀轉換。訪問時要處理跨段邊界,它可以防止未選中的塊數組類型被提前加載 。拿到第一個數據引用之後,不同的是 ,它的長度受 int大小限製。而且對任意 T來說也不一定合法
。
[MethodImpl(MethodImplOptions.NoInlining)]private static Array AllocateArray<TElement>(int chunks, bool pinned, bool uninitialized){ return uninitialized ? GC.AllocateUninitializedArray<TElement>(chunks, pinned) : GC.AllocateArray<TElement>(chunks, pinned);}這裏強行要求間接調用很關鍵。如果 index 、用戶不需要手動釋放內存 。
BigArray
有了塊機製之後 ,然後用普通的引用偏移往後移動 。這裏我們不需要在每次訪問時都除以塊大小 。它不擁有內存 ,
於是我決定自己做一個方案:
- 能容納超過 20 億個元素,塊結構體本身也可以組合。GitHub 上曾經有一個很長的 issue 討論 64 位數組支持
,
這就是
BigArray<T>的核心思路。真正的邏輯終點由_length記錄 。大小為 8 字節的類型可以使用 8,191。這時最後一個塊隻使用 1 個字節,對於object