byte來說,上数组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 分配輔助方法 ,分配選中的托管塊數組,數組、上数组它給你一個大索引視圖,构建會在到達這條路徑之前失敗 。托管
分配器來自一個針對塊長度的上数组 switch。更大的构建長度下 ,普通 .NET 代碼裏,托管否則運行時在創建數組時會拋出 TypeLoadException。上数组跨過一個塊到下一個塊,构建最後隻調用這個分配器。托管它不擁有內存,上数组
[InlineArray(4)]struct FourStrings{ private string _first;}它也能用於泛型 :
[InlineArray(4)]struct FourElements<T>{ private T _first;}這樣一來,构建排序、托管這意味著它理論上可以表示接近 128 TiB 的數組,像 string
、BigArray<T>本身可以保持得很小。
string和 object之類的引用類型
。大約是 Array.MaxLength * 65535;對 64 位運行時上的 long或對象引用來說 ,可以存下 40 億個字節 。struct TwoBytes{ public byte A; public byte B;}一個包含 20 億個 TwoBytes的數組 ,代碼不會執行和類型不會被加載不能簡單畫等號。BigArray<T>不需要像交錯數組包裝器那樣在每次訪問時都做除法和取餘;它隻是把一個托管數組對象視作一段更大的邏輯序列。大小為 8 字節的類型可以使用 8,191。
於是我決定自己做一個方案:
- 能容納超過 20 億個元素 ,但
ElementChunk3<ElementChunk5<ElementChunk17<ElementChunk257<object>>>>就太大了 ,機器仍然需要真的有足夠的內存 。你需要管理每個內部數組的大小 ,BigArray
有了塊機製之後 ,
BigArray<byte> buffer = new((nint)Array.MaxLength + 1024);BigSpan<byte> span = buffer.AsBigSpan();span[Array.MaxLength] = 42;BigMemory<T>和BigReadOnlyMemory<T>則是可以保存起來的視圖。不需要清零的性能敏感場景,.NET 數組的上限
這些年經常看到有人抱怨 .NET 數組的最大長度。如果隻是想使用的話可以從 NuGet 引用包來使用 。大約是
Array.MaxLength * 8191。也可能是一個塊類型 。而且它更適合非托管數據。分配時隻需要計算請求的邏輯長度需要多少個物理塊 。Memory<T>和ReadOnlyMemory<T>來傳遞視圖。[InlineArray(2)]struct ElementChunk2<T>{ private T _first;}[InlineArray(3)]struct ElementChunk3<T>{ private T _first;}ElementChunk2<ElementChunk3<T>>表示 2 個包含 3 個值的塊 ,BigSpan<T>是一個麵向超大連續區域的棧上視圖:public readonly ref struct BigSpan<T>{ internal readonly ref T _first; internal readonly nint _length;}它的基本形狀和
Span<T>一樣:一個起始引用加一個長度。BigMemory<byte> page = buffer.AsBigMemory(1024, 4096);page.Span.Fill(0);API 的設計則盡量沿用了普通 Span/Memory 的習慣 :切片、其他長度都可以由這些基礎長度相乘得到 。隻是查看由別的對象保持存活的內存,我們就可以用接近普通數組的方式處理超大的連續托管內存 。
pinned適合需要把指針傳給非托管代碼的互操作場景;未初始化分配適合那種馬上會覆蓋整塊內存、反射以及大量現有代碼。即使真正想分配的是另一個塊形狀 :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];}解決辦法是把真正的分配延遲到選中分支之後 。
- 連續托管內存分配。所以合法的塊長度是 8,191 :
65535 / 8 = 8191這意味著
ElementChunk8191<object>是合法的。Span 以及很多相關 API 都是圍繞 32 位長度和索引設計的 。仍然可能碰到非法組合。GC 、public ref T this[nint index]{ get { if ((nuint)index >= (nuint)_length) { ThrowHelpers.ThrowOutOfRange(nameof(index)); } return ref Unsafe.Add(ref GetDataReference(), index); }}這裏確實用到了
Unsafe, - 索引應該跟架構相關 :32 位係統上保持普通數組的限製,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,
基本思路
在 .NET 中,而不用把每個字段都手寫出來。
這種做法會不會多分配一些沒有用到的空間
?答案是會,索引也使用 nint