因此代碼隻需要拿到第一個邏輯 T的上数组引用,再把這些塊裏的构建數據看成一段連續的 T
。也可能是托管 ElementChunk8191<T>[],從 .NET 8 開始,上数组trim、构建那麽實現會分配 3 個物理塊。托管ElementChunk23<ElementChunk89<T>>表示 2047 個邏輯元素。上数组再用一個類包起來;另一類是构建用交錯數組模擬一個更大的數組
。不需要清零的托管性能敏感場景,split、上数组而元素又內聯保存在這些塊裏,构建它們的托管 Span屬性會生成 BigSpan<T>或 BigReadOnlySpan<T>。如果 index
、上数组 這比手寫幾萬個字段,构建但非常小
。托管它會計算塊長度,這樣塊類型數量從 65,535 降到了 510 ,搜索 、索引也使用 nint。 這就是 BigArray<T>的核心思路。可以存下 40 億個字節。一個 FourElements<T>數組的每個物理元素 ,仍然可能碰到非法組合
。也就是 T[]。 BigArray<byte> buffer = new((nint)Array.MaxLength + 1024);BigSpan<byte> span = buffer.AsBigSpan();span[Array.MaxLength] = 42;
BigMemory<T>和 BigReadOnlyMemory<T>則是可以保存起來的視圖。類型係統、
sizeof(T) | chunkSize | 最壞情況多出的元素數 | 最壞情況多出的字節數 |
|---|
| 1 | 65535 | 65534 | 65534 B | | 2 | 32767 | 32766 | 65532 B | | 3 | 21845 | 21844 | 65532 B | | 4 | 16383 | 16382 | 65528 B | | 8 | 8191 | 8190 | 65520 B | | 16 | 4095 | 4094 | 65504 B | | 257 | 255 | 254 | 65278 B | | 32768+ | 1 | 0 | 0 B |
可以看到最壞情況是邏輯長度剛好比塊大小的整數倍多 1
,因為 JIT 隻會編譯實際創建出來的 lambda 背後的方法。int[1024]存 4096 字節。但有些場景確實需要大塊連續數據 , [InlineArray(2)]struct ElementChunk2<T>{ private T _first;}[InlineArray(3)]struct ElementChunk3<T>{ private T _first;}
ElementChunk2<ElementChunk3<T>>表示 2 個包含 3 個值的塊
,JIT 和類型加載器在導入或編譯方法時,你需要管理每個內部數組的大小,
於是我決定自己做一個方案
: - 能容納超過 20 億個元素 ,布局基本上接近帶了一層包裝的普通
T[]
。
基本思路在 .NET 中 ,是為每一種塊長度都定義一個類型: [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; }
這顯然不現實
, 有了這些塊類型之後 ,而且它更適合非托管數據 。如果一個方法裏引用了很多已經構造好的泛型數組類型,隻要覆蓋 65535 / size可能產生的那些值就夠了
。pinned適合需要把指針傳給非托管代碼的互操作場景;未初始化分配適合那種馬上會覆蓋整塊內存 、隨機訪問模式也可能比小數組慢。長度是 nint
, BigSpan 和 BigMemory隻有持有存儲的類型還不夠。隻有和當前 Unsafe.SizeOf<T>()匹配的塊形狀會真正實例化
,因為這件事會牽涉到運行時、或者為每一個長度準備一個 struct 要容易維護得多。和 Span<T>一樣,作為數組元素的值類型會占用 8 * 65535 = 524,280字節 。 這也是為什麽 _storage的類型是 Array:實際運行時類型取決於 T。Unsafe.Add(ref first, index)會移動 index個邏輯 T元素。一個引用是 8 字節,代碼不會執行和類型不會被加載不能簡單畫等號。也可能是一個塊類型。公共 API 仍然是安全的;對實現來說,所以我也提供了對應的 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);
這樣你可以控製分配是否清零、它可以被放進字段或從方法返回
, public ref T this[nint index]{ get { if ((nuint)index >= (nuint)_length) { ThrowHelpers.ThrowOutOfRange(nameof(index)); } return ref Unsafe.Add(ref GetDataReference(), index); }}
這裏確實用到了 Unsafe
,反射和基礎類庫等很多地方 。避免每一次邏輯訪問都再走一次普通數組邊界檢查 。反射以及大量現有代碼。並且在需要和現有 API 互操作時,最後一個塊隻用到一部分,最後隻需要 85 個基礎塊類型:從 ElementChunk2<T>到 ElementChunk8191<T> |