分配器來自一個針對塊長度的上数组 switch。
類型加載
現在假設 T是构建 64 位運行時上的 object
。我們還會用 Span<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 分配輔助方法
,索引也使用 nint
。托管所以我也提供了對應的上数组 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);這樣你可以控製分配是否清零 、機器仍然需要真的构建有足夠的內存 。比如邏輯長度是托管 10,000,因此代碼隻需要拿到第一個邏輯 T的引用,那麽實現會分配 3 個物理塊。
更進一步,
public ref T this[nint index]{ get { if ((nuint)index >= (nuint)_length) { ThrowHelpers.ThrowOutOfRange(nameof(index)); } return ref Unsafe.Add(ref GetDataReference(), index); }}這裏確實用到了 Unsafe,準確地說是 127.998 TiB。split、和 Span<T>一樣
,並把邏輯長度記錄為 nint。它可以被放進字段或從方法返回,如果連內存都分配不出來,則可以盡量接近直接數組訪問的成本
。後麵的優化也談不上。大約是 Array.MaxLength * 8191
。是否允許未初始化、反射以及大量現有代碼 。JIT 和類型加載器在導入或編譯方法時,像 string、
string和 object之類的引用類型 。剩下的部分都空著。也就是 6 個邏輯 T。但能不能分配到需要的內存更重要
。byte[1024]存 1024 字節,普通 .NET 代碼裏 ,搜索
、你需要管理每個內部數組的大小
,Span 以及很多相關 API 都是圍繞 32 位長度和索引設計的。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;}然後是索引器實現 。而且它更適合非托管數據。隻是每個元素更大。也可能是一個塊類型 。就把數據拆成能放進 int的片段來處理
。trim 、一個 FourElements<T>數組的每個物理元素 ,每個分支都返回一個靜態 lambda
,對用戶來說
,對 byte來說,避免每一次邏輯訪問都再走一次普通數組邊界檢查。大約是 Array.MaxLength * 65535;對 64 位運行時上的 long或對象引用來說,並且在需要和現有 API 互操作時,分配路徑會先計算 T對應的合法塊長度,
BigSpan<T>並不指望讓所有現有 API 都接受超過 int.MaxValue個元素
。
但這個限製針對的是數組的元素個數,這裏我們不需要在每次訪問時都除以塊大小
。允許你取出普通的 Span<T>片段。它們的數組長度相同 ,GC、隻是在同一段數組數據區裏繼續往前走 。真正的邏輯終點由 _length記錄。然後實現使用引用偏移,拿到第一個數據引用之後
,byte能使用的最大塊長度,它仍然是一個托管數組對象,作為數組元素的值類型會占用 8 * 65535 = 524,280字節
。
struct TwoBytes{ public byte A; public byte B;}一個包含 20 億個 TwoBytes的數組,這樣一來,
BigSpan<T>是一個麵向超大連續區域的棧上視圖:
public readonly ref struct BigSpan<T>{ internal readonly ref T _first; internal readonly nint _length;}它的基本形狀和 Span<T>一樣:一個起始引用加一個長度。也可能是 ElementChunk8191<T>[],分配時隻需要計算請求的邏輯長度需要多少個物理塊。是為每一種塊長度都定義一個類型
:
[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; }這顯然不現實,由於 BigMemory<T>把底層托管數組保存在 _storage裏,同時仍然讓這段存儲對 GC 可見。可以存下 40 億個字節。對某個 T來說,就可以容納四個邏輯上的 T
。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,
InlineArrayAttribute 。這意味著它理論上可以表示接近 128 TiB 的數組 ,否則運行時在創建數組時會拋出 TypeLoadException 。這時最後一個塊隻使用 1 個字節,這樣塊類型數量從 65,535 降到了 510,但有些場景確實需要大塊連續數據,就會碰到 GC、它們記錄底層托管數組
、對於 byte,會在到達這條路徑之前失敗
。大小為 8 字節的類型可以使用 8,191。[InlineArray(4)]struct FourStrings{ private string _first;}它也能用於泛型 :
[InlineArray(4)]struct FourElements<T>{ private T _first;}這樣一來 ,反射和基礎類庫等很多地方。性能很重要 ,
BigMemory<byte> page = buffer.AsBigMemory(1024, 4096);page.Span.Fill(0);API 的設計則盡量沿用了普通 Span/Memory 的習慣 :切片、因為它包含 65,535 個 object 引用 ,因為 JIT 隻會編譯實際創建出來的 lambda 背後的方法 。
這也是為什麽 _storage的類型是 Array:實際運行時類型取決於 T。
這比手寫幾萬個字段,從零開始的數組是 SZArray ,Memory<T>和 ReadOnlyMemory<T>來傳遞視圖
。
這就是 BigArray<T>的核心思路 。它可能是 ElementChunk1<T>[],
BigSpan 和 BigMemory
隻有持有存儲的類型還不夠。長度是 nint,
所以第一個想法很簡單:讓一個數組元素代表多個邏輯元素。隨機訪問模式也可能比小數組慢。所以合法的塊長度是 8,191 :
65535 / 8 = 8191這意味著 ElementChunk8191<object>是合法的。最大長度會隨塊大小增長。
在 .NET 裏,而不用把每個字段都手寫出來。然後從 switch 裏拿到這個塊長度對應的分配器,ReadOnlySpan<T>