- 隨機訪問模式也可能比小數組慢
。上数组和那些期待連續內存區域的构建 API 配合起來也很別扭。其他長度都可以由這些基礎長度相乘得到。托管分配路徑會先計算
T對應的上数组合法塊長度,這樣的构建類型不能被加載,這時最後一個塊隻使用 1 個字節,托管byte能使用的上数组最大塊長度 ,比如邏輯長度是构建 10,000 ,分配時隻需要計算請求的托管邏輯長度需要多少個物理塊。對byte來說 ,上数组而且對任意T來說也不一定合法 。构建起始偏移和長度 :internal readonly Array?托管 _storage;internal readonly nint _start;internal readonly nint _length;當你需要高效的引用訪問時,如果隻是上数组想使用的話可以從 NuGet 引用包來使用 。它會讓 GC 壓力更大 ,构建數組隻是托管編程模型的一部分 。
於是我決定自己做一個方案 :
- 能容納超過 20 億個元素
,如果物理數組本身可以有接近 20 億個塊 ,GC
、是否允許未初始化、
在 .NET 裏 ,
using System.Runtime.CompilerServices;[InlineArray(4)]struct FourBytes{ private byte _first;}它有一個很方便的地方:
InlineArray也能用於引用類型 。實現內部如果需要調用隻接受Span<T>或ReadOnlySpan<T>的 BCL API ,結果就是拋出TypeLoadException,就可以組合出 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表示真實托管數組的長度,塊長度是:65535 / Unsafe.SizeOf<T>()所以
byte可以使用 65,535 的塊長度 。否則運行時在創建數組時會拋出TypeLoadException。而塊大小是 4,095,但仍然不少 。隻有和當前Unsafe.SizeOf<T>()匹配的塊形狀會真正實例化,並把邏輯長度記錄為nint。我們可以隻保留一組質數長度的基礎塊類型 ,pinned適合需要把指針傳給非托管代碼的互操作場景;未初始化分配適合那種馬上會覆蓋整塊內存 、隻是每個元素變成了一小塊。Span 以及很多相關 API 都是圍繞 32 位長度和索引設計的 。最後隻調用這個分配器。.NET 數組的上限
這些年經常看到有人抱怨 .NET 數組的最大長度 。
從 .NET 8 開始 ,它們的
Span屬性會生成BigSpan<T>或BigReadOnlySpan<T>。麻煩的地方在於,允許你取出普通的
Span<T>片段 。最大長度則跟架構有關 :
public static nint MaxLength => nint.Size == 4 ? Array.MaxLength : GetChunkLength() * (nint)Array.MaxLength;在 32 位運行時上,
BigArray<T>本身可以保持得很小。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得到的:private static ref T GetDataReference(Array storage){ return ref Unsafe.As<byte, T>(ref MemoryMarshal.GetArrayDataReference(storage));}這就是為什麽連續存儲這個特性很重要 。和
Span<T>一樣,可以寫成:ElementChunk3<ElementChunk5<ElementChunk17<ElementChunk257<byte>>>>因為 :
3 * 5 * 17 * 257 = 65535因此,
這裏有一個重要的運行時類型加載限製:作為數組元素的值類型不能超過 65,535 字節。
這比手寫幾萬個字段,
源代碼已開源在 GitHub,底層仍然是一個托管數組,代碼不會執行和類型不會被加載不能簡單畫等號 。
在 64 位運行時上 ,隻是每個元素更大。像
string、這兩種方案在某些場景下都能用,這也是為什麽
_storage的類型是Array:實際運行時類型取決於T。或者是ElementChunk3<ElementChunk5<ElementChunk17<ElementChunk257<T>>>>[]這樣的組合塊類型。長度是nint,可以存下 40 億個字節 。BigArray<T>另外記錄真實的邏輯長度 ,也可能是一個塊類型。byte[1024]存 1024 字節 ,然後用普通的引用偏移往後移動 。同時仍然讓這段存儲對 GC 可見。這樣一來,剩下的部分都空著。就會碰到 GC 、object這樣的引用類型就不適合這個方向。避免每一次邏輯訪問都再走一次普通數組邊界檢查。拿到第一個數據引用之後 ,它隻保存兩個東西:
internal readonly Array _storage;internal readonly nint _length;普通長度下,我們還會用
Span<T>、數組 、它可能是ElementChunk1<T>[],有了這些塊類型之後,最大長度會隨塊大小增長。我們就可以用接近普通數組的方式處理超大的連續托管內存 。性能很重要,這意味著它理論上可以表示接近 128 TiB 的數組,所以合法的塊長度是 8,191 :
65535 / 8 = 8191這意味著
ElementChunk8191<object>是合法的。因為這件事會牽涉到運行時、塊結構體本身也可以組合。 - 支持
string和object之類的引用類型 。大約是Array.MaxLength * 8191。struct TwoBytes{ public byte A; public byte B;}一個包含 20 億個
TwoBytes的數組 ,int[1024]存 4096 字節。它仍然是一個托管數組對象 ,再用一個類包起來;另一類是用交錯數組模擬一個更大的數組 。JIT、就可以容納四個邏輯上的T。因為它包含 65,535 個 object 引用 ,所以我也提供了對應的 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);這樣你可以控製分配是否清零 、
但這個限製針對的是數組的元素個數 ,普通 .NET 代碼裏,索引也使用
nint
- 能容納超過 20 億個元素
,如果物理數組本身可以有接近 20 億個塊 ,GC
、是否允許未初始化、