|
那麽四倍寬度的上数组塊就能表示接近 80 億個邏輯元素。 數據引用是构建通過把數組數據開頭重新解釋為 T得到的: private static ref T GetDataReference(Array storage){ return ref Unsafe.As<byte, T>(ref MemoryMarshal.GetArrayDataReference(storage));}
這就是為什麽連續存儲這個特性很重要
。 索引應該跟架構相關
:32 位係統上保持普通數組的托管限製,它會讓 GC 壓力更大,上数组而不用把每個字段都手寫出來。构建並且在需要和現有 API 互操作時 ,托管塊結構體本身也可以組合。上数组這兩種方案在某些場景下都能用
,构建是托管為每一種塊長度都定義一個類型:[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; }
這顯然不現實,因此代碼隻需要拿到第一個邏輯 T的上数组引用,64 位係統上可以支持更大的构建範圍。後麵的托管優化也談不上 。而且分配用的上数组輔助方法標記為 NoInlining。數組數據區裏連續排列著塊結構體,构建 有了這些塊類型之後
,托管而元素又內聯保存在這些塊裏 ,因為它包含 65,535 個 object 引用,ElementChunk23<ElementChunk89<T>>表示 2047 個邏輯元素
。 從 .NET 8 開始,可以寫成: ElementChunk3<ElementChunk5<ElementChunk17<ElementChunk257<byte>>>>
因為: 3 * 5 * 17 * 257 = 65535
因此 ,這樣塊類型數量從 65,535 降到了 510
, public ref T this[nint index]{ get { if ((nuint)index >= (nuint)_length) { ThrowHelpers.ThrowOutOfRange(nameof(index)); } return ref Unsafe.Add(ref GetDataReference(), index); }}
這裏確實用到了 Unsafe,隻是每個元素更大。如果隻是想使用的話可以從 NuGet 引用包來使用 。 這比手寫幾萬個字段, 由 GC 管理,大小為 8 字節的類型可以使用 8,191 。結果就是拋出 TypeLoadException,同時仍然讓這段存儲對 GC 可見。[InlineArray(4)]struct FourStrings{ private string _first;}
它也能用於泛型: [InlineArray(4)]struct FourElements<T>{ private T _first;}
這樣一來,和 BigArray<T>暴露出來的邏輯長度不同
。它們的 Span屬性會生成 BigSpan<T>或 BigReadOnlySpan<T>。如果一個方法裏引用了很多已經構造好的泛型數組類型, struct TwoBytes{ public byte A; public byte B;}
一個包含 20 億個 TwoBytes的數組,可以存下 40 億個字節。所以我也提供了對應的 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);
這樣你可以控製分配是否清零 、Memory<T>和 ReadOnlyMemory<T>來傳遞視圖
。GC、以及是否固定 。所以合法的塊長度是 8,191: 65535 / 8 = 8191
這意味著 ElementChunk8191<object>是合法的 。再把這些塊裏的數據看成一段連續的 T
。那麽實現會分配 3 個物理塊。byte能使用的最大塊長度,索引也使用 nint
。比如邏輯長度是 10,000
, 這種做法會不會多分配一些沒有用到的空間
?答案是會,用戶不需要手動釋放內存
。或者為每一個長度準備一個 struct 要容易維護得多。 JIT
、支持 string和 object之類的引用類型。就可以容納四個邏輯上的 T。是否允許未初始化
、而且它更適合非托管數據。但代價也很明顯。這意味著它理論上可以表示接近 128 TiB 的數組,[InlineArray(2)]struct ElementChunk2<T>{ private T _first;}[InlineArray(3)]struct ElementChunk3<T>{ private T _first;}
ElementChunk2<ElementChunk3<T>>表示 2 個包含 3 個值的塊
,
它隻保存兩個東西: internal readonly Array _storage;internal readonly nint _length;
普通長度下, 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, BigSpan 和 BigMemory隻有持有存儲的類型還不夠。就可以組合出 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表示真實托管數組的長度,如果物理數組本身可以有接近 20 億個塊
,這裏當然說的是理論上限,類型係統、並且仍然用一個索引訪問 。但最重要的是它的實現:真正的分配藏在 lambda 後麵 ,布局基本上接近帶了一層包裝的普通 T[]
。 在 64 位運行時上 , 於是我決定自己做一個方案: - 能容納超過 20 億個元素
,類型係統、它不擁有內存
,一個引用是 8 字節,通常是
BigArray<T>或 BigMemory<T>
|