我不知道作为一个集合的东西如何是不可变的并且仍然具有可接受的性能。
从我在F#集中读到的内部使用红黑树作为它们的实现。如果每次我们想要为Red Black Tree添加新东西,我们必须基本上重新创建它,它如何才能获得良好的性能?我在这里缺少什么?
虽然我问F#的集合,但我认为这与任何其他拥有或使用不可变数据结构的语言相关。
由于
答案 0 :(得分:38)
几乎所有不可变的集合都是某种形式的平衡树。要创建新树,您必须在路径上重新分配从更改(插入,删除,“更新”)到根的路径上的节点。只要树是平衡的,这需要对数时间。如果你有类似2-3-4树(类似于红黑树)的东西,预期的outdegree 3,你可以只使用10个分配来处理一百万个元素。
在预期数据结构纯粹的语言中,它们确保分配速度快。分配四元素节点将花费比较,增量和四个存储。在许多情况下,您可以通过多次分配来摊销比较成本。
如果你想进一步了解这些结构是如何运作的,那么Chris Okasaki就会提供一个很好的资料来源Purely Functional Data Structures。
答案 1 :(得分:19)
您不必重新创建整个树。许多分支机构将保持不变并可以“重复使用”。举个简单的例子,如果需要将新节点添加到当前树中的叶子上,那么只需要克隆该节点的父节点并给出新的分支。
答案 2 :(得分:12)
正如其他人指出的那样,您不必重新创建整个数据结构。您只需重新创建已更改的零件并引用保持不变的现有子树。由于数据结构的不变性,您可以重用子树,因此几乎不需要复制所有内容。实际上,如果您需要很少克隆可变数据结构,它可能会产生更大的影响。
特别是对于平行树(例如红黑树),这会给你:
这可能 - 当然 - 某些应用程序的开销太大,但实际上并不是那么糟糕。此外,.NET垃圾收集器中的分配非常快(我认为,基本上 O(1)),所以这不是一个真正的问题。更多的分配意味着GC需要更频繁地运行,但这也不像听起来那么重要 - 如今计算机拥有相当多的内存。在许多情况下,.NET 4.0实际上有所帮助(另请参阅Jon Harrop的answer here)
答案 3 :(得分:10)
正如其他人所说,不必完全重新创建不可变数据结构,因为它可以重用自身的旧部分。您可以这样做,因为旧部件是不可变的,并且保证数据不会改变。
我有一个不可改变的性能的真实世界的例子。我在F#中使用immutable Red Black tree进行了一些测试,它在c ++中只比std :: sort慢3倍。考虑到它并非专门用于排序,我认为这是非常快的。
答案 4 :(得分:4)
语言语义的限制仅适用于语言中的源代码。只要它保持相同的行为,实现(编译器,解释器,运行时环境等)就可以自由地做任何它想要的最佳性能。大多数语言都是如此。
编辑:
可以进行多项优化,包括数据共享(正是因为数据是不可变的),在幕后使用可变性,优化尾调用(因为FP使用大量递归)等等。
答案 5 :(得分:3)
见
functional programming: immutable data structure efficiency
(特别是我对Rich Hickey所说的回答)对于'一般'令人信服的证据,是的,不可变的结构也可以非常高效。
至于在F#Set
的特定情况下这是多么好,嗯,今天也许只是适度的。使用更有效的底层结构会很棒(用实用术语来说;理论上,当然一切都是O(logN)(实际上是O(1)))。
答案 6 :(得分:2)
不确定这是如何在语言中实现的,但数据结构可能被认为是程序员不可改变的,但在幕后进行优化。
例如,我有一个列表a = [1,2,3,4,5]。我追加6. b = [a [6]]并且它们都可以是不可变的。这样做不会失去任何性能,并且比复制值更快。所以,让我问你,因为我不知道,为什么做不变的事情会慢一点?在树的情况下,我有点看到你的观点。您必须重新创建当前节点上方的节点,但不是在下面(假设我们有子指针而不是父指针)。
答案 7 :(得分:2)
很简单,Set是基于节点的存储实体。在Set的情况下,您可以将其实现为树,其中当您将元素“添加”到下一版本的Set时,您不会重新创建所有边和节点,而只是创建一组新的边。你可以这样做,因为节点本身永远不会改变,对象也不会改变。
在单线程应用程序中发现它的真正好处,而在多线程应用程序中。不可变数据结构消除了对锁定机制的需要。如果他们永远不会改变,你就不必担心国家。