我不明白为什么Set作为一种不可变数据结构,仍然可以有良好的性能。
据我所了解,F#中的Sets内部使用红黑树作为实现方法。如果每次想要向红黑树中添加新元素都需要重新创建它,那么它怎么可能具有良好的性能呢?我在这里缺少了什么东西吗?
虽然我是为F#的Sets提出这个问题的,但我认为这对于任何其他具有或使用不可变数据结构的语言同样重要。
谢谢
我不明白为什么Set作为一种不可变数据结构,仍然可以有良好的性能。
据我所了解,F#中的Sets内部使用红黑树作为实现方法。如果每次想要向红黑树中添加新元素都需要重新创建它,那么它怎么可能具有良好的性能呢?我在这里缺少了什么东西吗?
虽然我是为F#的Sets提出这个问题的,但我认为这对于任何其他具有或使用不可变数据结构的语言同样重要。
谢谢
几乎所有不可变集合都是某种平衡树。要创建一个新的树,您必须重新分配从更改(插入、删除、“更新”)到根的路径上的节点。只要树是平衡的,这需要对数时间。如果您有类似于红黑树的2-3-4树,带有预期出度三,您可以使用仅10个分配处理100万个元素。
在那些期望数据结构是纯净的语言中,他们确保分配速度快。分配四个元素的节点将花费一次比较、一次增量和四次存储。在许多情况下,您可以分摊一次比较的成本到几个分配中。
如果您想了解这些结构如何工作的更多信息,请参阅Chris Okasaki的《纯函数式数据结构》。
您无需重新创建整个树。许多分支将保持不变,并可被“重复使用”。 举个简单的例子,如果新节点需要添加到当前树中的叶子上,则只需克隆该节点的父级并提供新的分支。
正如其他人指出的那样,您不必重新创建整个数据结构,只需重新创建已更改的部分并引用保持不变的现有子树。由于数据结构的不可变性,您可以重复使用子树,因此几乎永远不需要复制所有内容。实际上,如果您很少需要克隆可变数据结构,则可能会产生更高的影响。
特别是对于平衡树(例如红黑树),这为您提供:
这可能对某些应用程序来说过于繁琐,但实际上并没有那么糟糕。此外,.NET垃圾收集器中的分配非常快(我认为本质上是O(1)),因此这实际上不是问题。更多分配意味着GC需要更频繁地运行,但这也不像听起来那么关键,因为现代计算机有相当多的内存。 .NET 4.0在许多情况下实际上都有所帮助(另请参见Jon Harrop在此处的答案)。
正如其他人所说,不可变数据结构不必完全重新创建,因为它可以重用自己的旧部分。您可以这样做,因为旧部分是不可变的,并且保证数据不会更改。
我有一个不可变性能的现实世界示例。我使用F#制作了一个不可变红黑树并进行了一些测试,它的性能仅比c ++中的std :: sort慢3倍。 我认为这非常快,考虑到它并非专门设计用于排序。
请参考函数式编程:不可变数据结构的效率(尤其是我的回答,指出了Rich Hickey的演讲),以获取“一般”令人信服的证据,即不可变结构也可以非常高效。
至于在F# Set
特定情况下是否真的如此高效,今天可能只有中等水平。在实际情况下,使用更有效的基础结构将是很好的选择(在理论上当然是O(logN),但在实际情况下是O(1))。
我不确定这在语言中是如何实现的,但数据结构可以被程序员视为不可变的,而在幕后进行优化。
例如,我有一个列表a=[1,2,3,4,5]。我添加6。b=[a [6]],它们都可以是不可变的。这样做不会影响性能,并且比复制值更快。
那么,让我问你一个问题,因为我不知道,为什么使用不可变对象会更慢?在树的情况下,我有点理解你的观点。我想你得重新创建当前节点上面的节点,但下面的节点不用(假设我们有子指针而不是父指针)。
简单来说,Set是一种以节点为基础的存储实体。在Set中,您可以将其实现为树形结构,在其中添加元素到Set的下一个版本时,您不需要重新创建所有边缘和节点,而只需创建一组新的边缘。这样做是因为节点本身永远不会改变,里面持有的对象也不会改变。
真正的好处不在于单线程应用程序,而在于多线程应用程序中。不可变数据结构消除了锁定机制的需要。如果它们永远不会改变,您就不必担心状态。