高速 C# 不可變僅附加清單。 (更新:24 年 9 月 23 日;不建議使用。有關基本原理和替代方案,請參閱底部的註釋。)
dotnet 新增套件 AppendOnly
首先,現有的不可變集合很難讓您進行新增、刪除和更新以傳回新集合。他們確實必須複製很多東西。如果您只是追加,LinkedList 是您的朋友,並且可以立即「複製」。
其次,現有的不可變函式庫複雜且難以使用。它們需要建構器類別和對真實機制的良好理解,否則您將遇到嚴重的效能問題。
最後:這個類別在非常緊密的循環中使用是完全安全的。就像,盡可能緊。
AppendOnly 在內部使用 linkedList,並且在建立新的「不可變」副本時從不複製集合項目。它只是傳回一個新的集合類,該集合類共享相同的內部「可變」linkedList,但不公開任何方法來改變它,僅公開一個迭代器,該迭代器迭代集合中的前(n)項目。
因此,隨著集合變得越來越大,集合類別的早期(較舊)實例將永遠不會看到(透過枚舉器公開)附加到清單末尾的新條目。在將1000 個新的「交易」新增至appendOnly transactionCollection 後,您實際上擁有(如果保留對每個新建立的集合的引用)1000 個指向集合的指針,每個集合包含1、2、3、4等項目,但附加項接近零記憶體開銷。
如果將其他不可變程式庫與建構器一起使用,大多數程式庫都會為您提供至少 500 000 個物件實例。
我可能是錯的,這是一個起點,如果您使用它,請告訴我。我將沿著類似的思路創建更多的集合。
var move = new AppendOnlyList<Moves>();var new_moves = new AppendOnlyList<Moves>();Debug.Assert(moves.Length == 0);// 增加10 個隨機移動for(int i=0; i< 10 ; i++) new_moves = new_moves.Add(randomMove());// 即使 new_list 向 List 添加了一個項目,原始移動仍然沒有項目.Debug.Assert(moves.Length == 0);// new_list 有 10 item in it's collectionDebug.Assert(new_moves.Length == 10);// 這個清單可以安全地迭代多次,並且是線程安全的// 下面的程式碼顯示了整個清單的迭代十次,通常你只會這樣做/ / 一個可枚舉的,如果你已經快取了它,即創建了一個本地副本。 // 我知道這看起來並沒有什麼作用,但這確實很重要。 // 另外,當其他線程忙於添加到同一個底層集合時,執行此操作是安全的, // 這在線程世界中是一個巨大的“不”。在這裡,這是完全安全的。
在上面的程式碼中, new_moves
和原始moves
共享相同的底層鍊錶。當建立 AppendOnlyList 的新副本時,不會建立新的克隆,僅保留指向清單頭部的一個點,並且保留長度。
這是非常快的,因為沒有複製,所以它比任何其他創建副本的不可變集合要快得多。
當從其他線程枚舉集合時,它是線程安全的!繁榮! ……或者更具體地說「沒有繁榮!」。
這是版本0.1.3
,這意味著它尚未準備好用於生產使用。這是一個概念證明,我需要編寫一些線程測試來證明上述主張,然後請我的同行審查並讓我知道。
我還想對此集合和其他集合進行速度比較以及並排比較。
如果您喜歡這個包,請看一下實際的實現,它實際上是兩個類, AppendOnlyList
和LinkedListExtensions
,而且它們很小。
讓我知道你的想法?
:D
有任何疑問,請聯絡我,
Alan Hemmings, twitter : @snowcode LinkedIn : https://www.linkedin.com/in/goblinfactory www : https://goblinfactory.co.uk
……並沒有太多使用它,好吧,忘記了它。自從我寫這篇文章以來,事情 (C#) 已經發生了很大的變化,雖然 C# 發生了很多變化,但沒有一個真正簡化了事情。不幸的是,我認為我上面的想法,關於何時使用這個包,考慮到新的語言變化,可能是垃圾,並且可能......非常錯誤。在做進一步的事情之前,測試絕對是必要的。以下是與 AI C# 架構師快速交談的一些筆記,建議重新審視這個專案。說實話,我認為這個項目可能仍然是有保證的,但也許作為圍繞正確創建追加類型結構的不同方法的 DSL 包裝器(一種簡單工廠類)。納夫說;這是反饋;
(草稿筆記,需要測試、確認!)
是的,在 C# 中,您可以使用System.Collections.Generic.ArrayBuffer<T>
或System.Buffers.ArrayPool<T>
來獲得更有效率的僅附加和記憶體高效結構,並且可以有效地枚舉。但是,如果您想要更實用、更有效率的僅附加結構,請考慮以下選項:
ImmutableArray<T>.Builder
System.Collections.Immutable
套件提供的僅附加結構。
它允許高效附加,並且在建置時是可變的,但一旦轉換為ImmutableArray<T>
就變得不可變。
var builder = ImmutableArray.CreateBuilder<int>();builder.Add(1);builder.Add(2);var immutableArray = builder.ToImmutable();
Memory<T>
和Span<T>
與ArrayPool<T>
以最少的記憶體分配有效處理大數據塊。
您自己管理內存,與List<T>
相比,這可以避免頻繁的重新分配。
如果您想要更多地控制記憶體使用但需要仔細管理,則適合。
var pool = ArrayPool<int>.Shared;int[] array = pool.Rent(1000); // 從池中請求一個陣列 // 在這裡追加並管理您的資料pool.Return(array); // 完成後返回池
ValueListBuilder<T>
(來自 System.Buffers)
提供了一種以堆疊分配方式建構集合的有效方法。
當您知道資料大小是有限的並且希望在沒有堆分配的情況下獲得高效能時特別有用。
Span<int> 初始Span = stackalloc int[16]; var valueListBuilder = new ValueListBuilder<int>(initialSpan);valueListBuilder.Append(1);valueListBuilder.Append(2);ReadOnlySpan<int> span = valueListBuilder.AsSpan();
System.IO.Pipelines 中的Sequence<T>
適合以僅附加方式有效處理資料序列。
適用於需要高效能 I/O 操作和串流資料的場景。
List<T>
:適合追加(攤銷O(1)
),並且由於連續記憶體而提供快速枚舉。
ImmutableArray<T>.Builder
:對於追加然後使其不可變非常有效。
Memory<T>
和ArrayPool<T>
:非常適合減少分配。
ValueListBuilder<T>
:當大小已知時,基於堆疊的高效率建置。基於堆疊,適合臨時、有效率的收集。
Sequence<T>
:適用於需要高效率追加的高吞吐量場景。
根據您的特定效能和記憶體需求進行選擇。
ChatGPT 說;
C# 中的LinkedList<T>
是僅追加記憶體結構的選項,但與其他結構相比,它通常記憶體效率較低且枚舉速度較慢,原因如下:
LinkedList<T>
的注意事項:記憶體開銷: LinkedList<T>
中的每個節點都包含對下一個和上一個節點的引用,與數組或List<T>
等連續結構相比,這會帶來顯著的記憶體開銷。
快取效率低:與儲存在連續記憶體中的陣列或List<T>
不同, LinkedList<T>
節點分散在堆中。這使得它不太適合緩存,導致迭代和訪問時間變慢。
枚舉效能:雖然LinkedList<T>
對於追加操作非常有效率(添加到末尾的時間複雜度O(1)
),但其枚舉效能通常比陣列或清單慢,因為它缺乏快取局部性優勢。
LinkedList<T>
:當需要在兩端或中間頻繁插入或刪除時。
如果您想避免像List<T>
中那樣移動元素。
LinkedList<T>
可用於僅追加場景,但與List<T>
或其他現代資料結構相比,通常不是枚舉或記憶體使用的最有效選擇。