托管數在 組 上構建超大
TypeLoadException
。上数组以及是构建否固定 。Memory<T>和 ReadOnlyMemory<T>來傳遞視圖。托管搜索
、上数组類型加載
現在假設 T是构建 64 位運行時上的 object。是托管否允許未初始化 、
[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);}這裏強行要求間接調用很關鍵
。這意味著它理論上可以表示接近 128 TiB 的构建數組
,nint本身無法表示更大的托管索引空間,
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 分配輔助方法,即使真正想分配的构建是另一個塊形狀 :
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];}解決辦法是把真正的分配延遲到選中分支之後。剩下的托管部分都空著。但有些場景確實需要大塊連續數據
,上数组性能很重要 ,构建ReadOnlySpan<T>、托管因此代碼隻需要拿到第一個邏輯 T的引用,
手動管理內存很容易出錯
,真正的邏輯終點由 _length記錄。反射和基礎類庫等很多地方
。最後隻需要 85 個基礎塊類型 :從 ElementChunk2<T>到 ElementChunk8191<T>。數組、並且仍然用一個索引訪問
。
在 .NET 裏,由於 BigMemory<T>把底層托管數組保存在 _storage裏,也就是 T[]。分配路徑會先計算 T對應的合法塊長度,它可以防止未選中的塊數組類型被提前加載 。但本質上仍然是一組數組。想要直接放寬這個限製,數組隻是編程模型的一部分 。object這樣的引用類型就不適合這個方向 。trim、像 string、
[InlineArray(4)]struct FourStrings{ private string _first;}它也能用於泛型 :
[InlineArray(4)]struct FourElements<T>{ private T _first;}這樣一來,但它隻藏在實現內部。大約是 Array.MaxLength * 8191 。或者是 ElementChunk3<ElementChunk5<ElementChunk17<ElementChunk257<T>>>>[]這樣的組合塊類型。起始偏移和長度 :
internal readonly Array? _storage;internal readonly nint _start;internal readonly nint _length;當你需要高效的引用訪問時,
BigArray
有了塊機製之後,它仍然是一個托管數組對象 ,
struct TwoBytes{ public byte A; public byte B;}一個包含 20 億個 TwoBytes的數組,排序、BigArray<T>另外記錄真實的邏輯長度
,大小為 8 字節的類型可以使用 8,191
。
在 64 位運行時上,這裏當然說的是理論上限,而不是元素背後的字節數。而且它更適合非托管數據。
麻煩的地方在於,實現內部如果需要調用隻接受 Span<T>或 ReadOnlySpan<T>的 BCL API ,它會計算塊長度,長度是 nint,
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,它的長度受 int大小限製
。拿到第一個數據引用之後 ,它不擁有內存,後麵的優化也談不上。但數組元素類型不一定是 T本身
,尤其是在大分配的情況下。因為 JIT 隻會編譯實際創建出來的 lambda 背後的方法
。隻是在同一段數組數據區裏繼續往前走。我們有了 InlineArrayAttribute。就可以容納四個邏輯上的 T
。對於 byte,它會讓 GC 壓力更大,它給你一個大索引視圖,這時最後一個塊隻使用 1 個字節 ,也可能是 ElementChunk8191<T>[],則可以盡量接近直接數組訪問的成本。
[InlineArray(2)]struct ElementChunk2<T>{ private T _first;}[InlineArray(3)]struct ElementChunk3<T>{ private T _first;}ElementChunk2<ElementChunk3<T>>表示 2 個包含 3 個值的塊,類型係統
、隻是每個元素更大。每個分支都返回一個靜態 lambda,但 ElementChunk3<ElementChunk5<ElementChunk17<ElementChunk257<object>>>>就太大了,可以寫成:
ElementChunk3<ElementChunk5<ElementChunk17<ElementChunk257<byte>>>>因為:
3 * 5 * 17 * 257 = 65535因此 ,布局基本上接近帶了一層包裝的普通 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 ,
這也意味著實現不需要為每一個整數都準備一個塊類型。確定這個值之後 ,它們的 Span屬性會生成 BigSpan<T>或 BigReadOnlySpan<T>。
BigMemory<byte> page = buffer.AsBigMemory(1024, 4096);page.Span.Fill(0);API 的設計則盡量沿用了普通 Span/Memory 的習慣:切片、ToArray 、你需要管理每個內部數組的大小
,
這種做法會不會多分配一些沒有用到的空間
?答案是會 ,底層是一個托管數組,對 byte來說,但它不會在 object路徑上被加載。而且對任意 T來說也不一定合法
。反射以及大量現有代碼
。byte[1024]存 1024 字節,塊長度是
:
65535 / Unsafe.SizeOf<T>()所以 byte可以使用 65,535 的塊長度。pinned適合需要把指針傳給非托管代碼的互操作場景;未初始化分配適合那種馬上會覆蓋整塊內存
、
這就是 BigArray<T>的核心思路。
這比手寫幾萬個字段,
這也是為什麽 _storage的類型是 Array
:實際運行時類型取決於 T。
BigArray<T>暴露出來的邏輯長度不同。塊結構體本身也可以組合。而且塊大小是 65,535
。它可能是 ElementChunk1<T>[]
,源代碼已開源在 GitHub,代碼會選擇 8191分支並創建 ElementChunk8191<object>[];65535分支仍然存在給用於 byte這樣的類型使用 ,可以存下 40 億個字節 。比如邏輯長度是 10,000 ,對某個 T來說,
寫在最後
有了 BigArray<T>
