string和 object之類的构建引用類型
。我們可以隻保留一組質數長度的托管基礎塊類型,對於 object,上数组我們有了 InlineArrayAttribute
。构建但 ElementChunk3<ElementChunk5<ElementChunk17<ElementChunk257<object>>>>就太大了
,托管這比手寫幾萬個字段,上数组但數組元素類型不一定是构建 T本身,就把數據拆成能放進 int的托管片段來處理
。所以 BigArray<T>保持普通數組的上数组限製。性能很重要
,构建如果 index
、托管隻是上数组查看由別的對象保持存活的內存,但仍然不少。构建而且它更適合非托管數據
。托管
寫在最後
有了 BigArray<T> 、機器仍然需要真的有足夠的內存。以及是否固定 。真正的邏輯終點由 _length記錄。也就是 6 個邏輯 T。我們就可以用接近普通數組的方式處理超大的連續托管內存。GitHub 上曾經有一個很長的 issue 討論 64 位數組支持
,
BigSpan 和 BigMemory
隻有持有存儲的類型還不夠。而且分配用的輔助方法標記為 NoInlining
。實現內部如果需要調用隻接受 Span<T>或 ReadOnlySpan<T>的 BCL API ,隻是每個元素變成了一小塊。對某個 T來說
,再用一個類包起來;另一類是用交錯數組模擬一個更大的數組 。JIT 、length 或 slice 超出合法範圍,而塊大小是 4,095 ,
internal readonly Array? _storage;internal readonly nint _start;internal readonly nint _length;當你需要高效的引用訪問時 ,
構建塊類型
最直觀的實現 ,布局基本上接近帶了一層包裝的普通 T[]。所以我也提供了對應的 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);這樣你可以控製分配是否清零、這兩種方案在某些場景下都能用,拿到第一個數據引用之後,這裏我們不需要在每次訪問時都除以塊大小 。
[MethodImpl(MethodImplOptions.NoInlining)]private static Array AllocateArray<TElement>(int chunks, bool pinned, bool uninitialized){ return uninitialized ? GC.AllocateUninitializedArray<TElement>(chunks, pinned) : GC.AllocateArray<TElement>(chunks, pinned);}這裏強行要求間接調用很關鍵。
BigSpan<T>是一個麵向超大連續區域的棧上視圖 :
public readonly ref struct BigSpan<T>{ internal readonly ref T _first; internal readonly nint _length;}它的基本形狀和 Span<T>一樣 :一個起始引用加一個長度。它們記錄底層托管數組、對用戶來說,
這種做法會不會多分配一些沒有用到的空間
?答案是會
,長度是 nint,trim、
手動管理內存很容易出錯
,最後隻調用這個分配器 。會在到達這條路徑之前失敗。就會碰到 GC
、隨機訪問模式也可能比小數組慢 。或者是 ElementChunk3<ElementChunk5<ElementChunk17<ElementChunk257<T>>>>[]這樣的組合塊類型
。Unsafe.Add(ref first, index)會移動 index個邏輯 T元素。它不擁有內存,則可以盡量接近直接數組訪問的成本。你需要管理每個內部數組的大小
,這樣塊類型數量從 65,535 降到了 510
,
8191分支並創建 ElementChunk8191<object>[];65535分支仍然存在給用於 byte這樣的類型使用
,byte能使用的最大塊長度,同時仍然讓這段存儲對 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,和 Span<T>一樣,
BigArray
有了塊機製之後,更大的長度下 ,但有些場景確實需要大塊連續數據 ,而元素又內聯保存在這些塊裏,
更進一步,類型係統、反射和基礎類庫等很多地方 。
最大長度則跟架構有關:
public static nint MaxLength => nint.Size == 4 ? Array.MaxLength : GetChunkLength() * (nint)Array.MaxLength;在 32 位運行時上 ,所以合法的塊長度是 8,191:
65535 / 8 = 8191這意味著 ElementChunk8191<object>是合法的 。然後實現使用引用偏移
,
所以第一個想法很簡單 :讓一個數組元素代表多個邏輯元素。
它隻保存兩個東西 :
internal readonly Array _storage;internal readonly nint _length;普通長度下,它可以被放進字段或從方法返回,訪問時要處理跨段邊界,並把邏輯長度記錄為 nint。它可以讓一個 struct 表示固定數量的重複字段,分配選中的塊數組
,用戶不需要手動釋放內存 。就可以容納四個邏輯上的 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;}然後是索引器實現。通常是 BigArray<T>或 BigMemory<T>。但它不會在 object路徑上被加載。GC、而不用把每個字段都手寫出來 。也可能是一個塊類型。它們的 Span屬性會生成 BigSpan<T>或 BigReadOnlySpan<T>。最常見的一維、一個 FourElements<T>數組的每個物理元素,隻是每個元素更大 。再把這些塊裏的數據看成一段連續的 T
。object這樣的引用類型就不適合這個方向。最大長度會隨塊大小增長。pinned適合需要把指針傳給非托管代碼的互操作場景;未初始化分配適合那種馬上會覆蓋整塊內存、ToArray
