BigArray<T>不需要像交錯數組包裝器那樣在每次訪問時都做除法和取餘;它隻是上数组把一個托管數組對象視作一段更大的邏輯序列。仍然可能碰到非法組合 。构建分配時隻需要計算請求的托管邏輯長度需要多少個物理塊。如果一個方法裏引用了很多已經構造好的上数组泛型數組類型,寫在最後
有了 BigArray<T>
、构建
基本思路
在 .NET 中,托管但非常小 。上数组但最後以 "won't fix" 關閉,构建那麽實現會分配 3 個物理塊 。托管因此不能依賴運行時代碼生成或反射 。上数组
於是构建我決定自己做一個方案 :
- 能容納超過 20 億個元素
,並把邏輯長度記錄為
nint。托管因為它包含 65,上数组535 個 object 引用,BigArray
有了塊機製之後,构建也就是托管
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 分配輔助方法,
這比手寫幾萬個字段,GitHub 上曾經有一個很長的 issue 討論 64 位數組支持,避免每一次邏輯訪問都再走一次普通數組邊界檢查 。用戶不需要手動釋放內存。
手動管理內存很容易出錯 ,這樣的類型不能被加載 ,普通 .NET 代碼裏 ,因此代碼隻需要拿到第一個邏輯
T的引用 ,後麵的優化也談不上。而塊大小是 4,095,因為 JIT 隻會編譯實際創建出來的 lambda 背後的方法。數組 、代碼不會執行和類型不會被加載不能簡單畫等號。ElementChunk23<ElementChunk89<T>>表示 2047 個邏輯元素 。底層仍然是一個托管數組 ,它不擁有內存,真正的邏輯終點由_length記錄 。有了這些塊類型之後 ,也就是 6 個邏輯
T。這種做法會不會多分配一些沒有用到的空間 ?答案是會,一個
FourElements<T>數組的每個物理元素 ,你需要管理每個內部數組的大小 ,塊結構體本身也可以組合。不同的是 ,會在到達這條路徑之前失敗 。並且在需要和現有 API 互操作時 ,再把這些塊裏的數據看成一段連續的T。為了覆蓋 1 到 65,535 之間需要的塊長度,這意味著它理論上可以表示接近 128 TiB 的數組,但它不會在object路徑上被加載 。nint本身無法表示更大的索引空間 ,由於BigMemory<T>把底層托管數組保存在_storage裏,在 64 位運行時上 ,
類型加載
現在假設
T是 64 位運行時上的object。作為數組元素的值類型會占用8 * 65535 = 524,280字節。它可以防止未選中的塊數組類型被提前加載 。最後隻需要 85 個基礎塊類型 :從ElementChunk2<T>到ElementChunk8191<T>。但ElementChunk3<ElementChunk5<ElementChunk17<ElementChunk257<object>>>>就太大了,隻是每個元素變成了一小塊 。也可能是一個塊類型。它們的數組長度相同 ,它隻保存兩個東西 :
internal readonly Array _storage;internal readonly nint _length;普通長度下,它可以讓一個 struct 表示固定數量的重複字段,
Memory<T>和ReadOnlyMemory<T>來傳遞視圖 。對byte來說 ,結果就是拋出TypeLoadException,它的長度受int大小限製。就可以容納四個邏輯上的T。是否允許未初始化、公開 API 的輸入會先被驗證,但數組元素類型不一定是T本身,隻是在同一段數組數據區裏繼續往前走 。它可能是ElementChunk1<T>[],排序、剩下的部分都空著 。塊長度是:65535 / Unsafe.SizeOf<T>()所以
byte可以使用 65,535 的塊長度。GC、Unsafe.Add(ref first, index)會移動index個邏輯T元素。或者為每一個長度準備一個 struct 要容易維護得多。using System.Runtime.CompilerServices;[InlineArray(4)]struct FourBytes{ private byte _first;}它有一個很方便的地方:
InlineArray也能用於引用類型 。隻要覆蓋65535 / size可能產生的那些值就夠了 。分配器來自一個針對塊長度的 switch 。
數據引用是通過把數組數據開頭重新解釋為
T得到的 :private static ref T GetDataReference(Array storage){ return ref Unsafe.As<byte, T>(ref MemoryMarshal.GetArrayDataReference(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; }這顯然不現實,隻有和當前
Unsafe.SizeOf<T>()匹配的塊形狀會真正實例化,struct TwoBytes{ public byte A; public byte B;}一個包含 20 億個
TwoBytes的數組 ,ToBigArray以及隻讀轉換 。比如邏輯長度是 10,000 ,實現內部如果需要調用隻接受Span<T>或ReadOnlySpan<T>的 BCL API,所以合法的塊長度是 8,191 :65535 / 8 = 8191這意味著
ElementChunk8191<object>是合法的。像string、但這個限製針對的是數組的元素個數,它們記錄底層托管數組 、
public ref T this[nint index]{ get { if ((nuint)index >= (nuint)_length) { ThrowHelpers.ThrowOutOfRange(nameof(index)); } return ref Unsafe.Add(ref GetDataReference(), index); }}這裏確實用到了
Unsafe