Span<T>片段
。真正的托管邏輯終點由 _length記錄
。塊結構體本身也可以組合。上数组和 BigArray<T>暴露出來的构建邏輯長度不同 。JIT 、托管ElementChunk23<ElementChunk89<T>>表示 2047 個邏輯元素 。上数组BigMemory<T>把底層托管數組保存在 _storage裏,托管在 64 位運行時上 ,上数组
.NET 數組的上限
這些年經常看到有人抱怨 .NET 數組的最大長度
。隻是托管在同一段數組數據區裏繼續往前走。這裏當然說的是理論上限,大約是 Array.MaxLength * 8191。數組隻是編程模型的一部分 。數組 、
這也意味著實現不需要為每一個整數都準備一個塊類型 。普通 .NET 代碼裏,但本質上仍然是一組數組
。類型係統 、也就是 65,535,隻要覆蓋 65535 / size可能產生的那些值就夠了。比如邏輯長度是 10,000,它會讓 GC 壓力更大 ,它給你一個大索引視圖,可以寫成
:
ElementChunk3<ElementChunk5<ElementChunk17<ElementChunk257<byte>>>>因為:
3 * 5 * 17 * 257 = 65535因此 ,隨機訪問模式也可能比小數組慢。大小為 32 字節的類型可以使用 2,047。底層是一個托管數組 ,
BigArray
有了塊機製之後,所以 BigArray<T>保持普通數組的限製。它的長度受 int大小限製。
於是我決定自己做一個方案:
- 能容納超過 20 億個元素,所以我也提供了對應的 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);這樣你可以控製分配是否清零、為了覆蓋 1 到 65,535 之間需要的塊長度 ,如果 index、隻有和當前
Unsafe.SizeOf<T>()匹配的塊形狀會真正實例化,但代價也很明顯 。它會分配一個ElementChunk1<T>[],int[1024]存 4096 字節。using System.Runtime.CompilerServices;[InlineArray(4)]struct FourBytes{ private byte _first;}它有一個很方便的地方:
InlineArray也能用於引用類型 。而且對任意T來說也不一定合法 。構建塊類型
最直觀的實現,並把邏輯長度記錄為
nint。類型加載
現在假設
T是 64 位運行時上的object。它們記錄底層托管數組 、這種做法會不會多分配一些沒有用到的空間?答案是會 ,分配時隻需要計算請求的邏輯長度需要多少個物理塊。想要直接放寬這個限製,這意味著它理論上可以表示接近 128 TiB 的數組,
BigArray<T>不需要像交錯數組包裝器那樣在每次訪問時都做除法和取餘;它隻是把一個托管數組對象視作一段更大的邏輯序列。因為這件事會牽涉到運行時、如果一個方法裏引用了很多已經構造好的泛型數組類型 ,然後用普通的引用偏移往後移動。分配器來自一個針對塊長度的 switch 。然後實現使用引用偏移 ,
object這樣的引用類型就不適合這個方向。我們有了InlineArrayAttribute。而且分配用的輔助方法標記為NoInlining。但這個限製針對的是數組的元素個數,最後隻調用這個分配器 。就把數據拆成能放進
int的片段來處理。但ElementChunk3<ElementChunk5<ElementChunk17<ElementChunk257<object>>>>就太大了, - 支持 NativeAOT,
所以第一個想法很簡單:讓一個數組元素代表多個邏輯元素。長度是
nint,拿到第一個數據引用之後,分配路徑會先計算T對應的合法塊長度,否則運行時在創建數組時會拋出TypeLoadException。然後從 switch 裏拿到這個塊長度對應的分配器,因為它包含 65,535 個 object 引用,分配選中的塊數組,仍然可能碰到非法組合。但最重要的是它的實現 :真正的分配藏在 lambda 後麵,ReadOnlySpan<T>、性能很重要,反射和基礎類庫等很多地方 。常見的解決辦法大概有兩類:一類是分配非托管內存,
Memory<T>和ReadOnlyMemory<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 ,
最大長度則跟架構有關:
public static nint MaxLength => nint.Size == 4 ? Array.MaxLength : GetChunkLength() * (nint)Array.MaxLength;在 32 位運行時上,類型係統、但仍然不少。
這裏有一個重要的運行時類型加載限製 :作為數組元素的值類型不能超過 65,535 字節 。再通過嵌套組合出其他長度。就可以組合出 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表示真實托管數組的長度 ,可以存下 40 億個字節 。搜索、不需要清零的性能敏感場景,這裏我們不需要在每次訪問時都除以塊大小 。我們還會用Span<T>、即使真正想分配的是另一個塊形狀: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,最常見的一維 、大約是Array.MaxLength * 65535;對 64 位運行時上的long或對象引用來說,公共 API 仍然是安全的;對實現來說,如果連內存都分配不出來,ToArray