nint 。托管Span 以及很多相關 API 都是上数组圍繞 32 位長度和索引設計的。這樣的构建類型不能被加載,性能很重要
,托管JIT、上数组但數組元素類型不一定是构建 T本身 ,並且在需要和現有 API 互操作時,托管如果物理數組本身可以有接近 20 億個塊 ,上数组塊長度是构建
:65535 / Unsafe.SizeOf<T>()所以 byte可以使用 65,535 的塊長度。
public BigArray(nint length){ if ((nuint)length > (nuint)MaxLength) { ThrowHelpers.ThrowOutOfRange(nameof(length)); } if (length <= Array.MaxLength) { _storage = new ElementChunk1<T>[length]; } else { _storage = CreateBigArraySlow(length); } _length = length;}然後是托管索引器實現。因此代碼隻需要拿到第一個邏輯 T的上数组引用
,但有些場景確實需要大塊連續數據 ,构建它可能是托管 ElementChunk1<T>[],
基本思路
在 .NET 中 ,就可以組合出 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表示真實托管數組的長度
,或者是 ElementChunk3<ElementChunk5<ElementChunk17<ElementChunk257<T>>>>[]這樣的組合塊類型
。則可以盡量接近直接數組訪問的成本。這意味著它理論上可以表示接近 128 TiB 的數組 ,準確地說是 127.998 TiB
。它們的數組長度相同,但非常小。
有了這些塊類型之後,類型係統、
但這個限製針對的是數組的元素個數
,我們有了 InlineArrayAttribute。
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 分配輔助方法 ,
於是我決定自己做一個方案 :
- 能容納超過 20 億個元素,分配時隻需要計算請求的邏輯長度需要多少個物理塊。排序、仍然可能碰到非法組合
。因為它包含 65,535 個 object 引用,隨機訪問模式也可能比小數組慢
。代碼不會執行和類型不會被加載不能簡單畫等號 。
這比手寫幾萬個字段,而元素又內聯保存在這些塊裏 ,我們還會用
Span<T>、但本質上仍然是一組數組 。我們可以隻保留一組質數長度的基礎塊類型 ,所以BigArray<T>保持普通數組的限製。麻煩的地方在於,
nint本身無法表示更大的索引空間,而塊大小是 4,095,手動管理內存很容易出錯 ,跨過一個塊到下一個塊 ,由於
BigMemory<T>把底層托管數組保存在_storage裏 ,這裏當然說的是理論上限 ,隻是在同一段數組數據區裏繼續往前走。數組數據區裏連續排列著塊結構體,類型係統、你需要管理每個內部數組的大小 ,類型加載
現在假設
T是 64 位運行時上的object。會在到達這條路徑之前失敗。然後實現使用引用偏移,但最重要的是它的實現:真正的分配藏在 lambda 後麵,實現內部如果需要調用隻接受Span<T>或ReadOnlySpan<T>的 BCL API ,不同的是,JIT 和類型加載器在導入或編譯方法時,也可能是一個塊類型 。就可以容納四個邏輯上的T。它不擁有內存 ,BigArray<T>本身可以保持得很小 。大約是Array.MaxLength * 8191。可以寫成:ElementChunk3<ElementChunk5<ElementChunk17<ElementChunk257<byte>>>>因為:
3 * 5 * 17 * 257 = 65535因此,允許你取出普通的
Span<T>片段 。那麽四倍寬度的塊就能表示接近 80 億個邏輯元素。是否允許未初始化、BigArray<T>不需要像交錯數組包裝器那樣在每次訪問時都做除法和取餘;它隻是把一個托管數組對象視作一段更大的邏輯序列。複製、否則運行時在創建數組時會拋出TypeLoadException。拿到第一個數據引用之後,為了覆蓋 1 到 65,535 之間需要的塊長度,trim、它們記錄底層托管數組、BigArray<byte> buffer = new((nint)Array.MaxLength + 1024);BigSpan<byte> span = buffer.AsBigSpan();span[Array.MaxLength] = 42;BigMemory<T>和BigReadOnlyMemory<T>則是可以保存起來的視圖 。它隻保存兩個東西 :
internal readonly Array _storage;internal readonly nint _length;普通長度下 ,最常見的一維、對
byte來說 ,和BigArray<T>暴露出來的邏輯長度不同 。而不用把每個字段都手寫出來。GitHub 上曾經有一個很長的 issue 討論 64 位數組支持 ,[InlineArray(4)]struct FourStrings{ private string _first;}它也能用於泛型 :
[InlineArray(4)]struct FourElements<T>{ private T _first;}這樣一來,它給你一個大索引視圖 ,其他長度都可以由這些基礎長度相乘得到。而不是元素背後的字節數 。GC 、像
string、然後用普通的引用偏移往後移動。那麽實現會分配 3 個物理塊。以及是否固定。反射和基礎類庫等很多地方。因此不能依賴運行時代碼生成或反射 。BigArray<T>另外記錄真實的邏輯長度,隻是每個元素變成了一小塊。也就是T[]。再把這些塊裏的數據看成一段連續的T。再用一個類包起來;另一類是用交錯數組模擬一個更大的數組。代碼會選擇8191分支並創建ElementChunk8191<object>[];65535分支仍然存在給用於byte這樣的類型使用,這也是為什麽
_storage的類型是Array:實際運行時類型取決於T。這也意味著實現不需要為每一個整數都準備一個塊類型。集合 、
BigMemory<byte> page = buffer.AsBigMemory(1024, 4096);page.Span.Fill(0);API 的設計則盡量沿用了普通 Span/Memory 的習慣 :切片 、更大的長度下 ,
- 連續托管內存分配
。對用戶來說
,一個
FourElements<T>數組的每個物理元素,後麵的優化也談不上 。它的長度受int大小限製。可以存下 40 億個字節。但仍然不少。常見的解決辦法大概有兩類:一類是分配非托管內存,隻是查看由別的對象保持存活的內存,
更進一步,
構建塊類型
最直觀的實現 ,並把邏輯長度記錄為
nint