高速 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 个项目Debug.Assert(new_moves.Length == 10);// 这个列表可以安全地迭代多次并且是线程安全的//下面的代码显示了对整个列表的迭代十次,通常情况下,如果您已经缓存了可枚举对象,则只会对它执行此操作,即创建一个本地副本。 // 我知道这一点看起来作用不大,但这确实很重要。 // 另外,当其他线程忙于添加到同一个底层集合时,执行此操作是安全的, // 这在线程世界中是一个巨大的“不”。在这里,这是完全安全的。for(int i = 0; i<10; i++){foreach(var move in new_moves.Items) wait DoSomethingFunkyAsync(move);}
在上面的代码中, 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>
或其他现代数据结构相比,通常不是枚举或内存使用的最有效选择。