用于大量连接和单次迭代的最快不可变列表数据结构

时间:2011-12-13 18:09:45

标签: haskell data-structures functional-programming immutability

我正在使用Haskell。标准列表连接是天真和缓慢的。我的情况是我有一个算法,它建立一个单独的列表连接(顺序无关紧要,所以它可以是前置或附加或组合)多次,然后返回它。结果将只使用一次。高绩效至关重要。

所以,这是一个非常简单的情况。我听说过差异列表,这有助于解决这种情况。但那是最好的选择吗?

列表可能会变得很大:100,000个条目。

5 个答案:

答案 0 :(得分:15)

这是一个经验问题,应该凭经验回答。合理的替代方案包括

  • 有缺点的标准清单(在您的问题中称为“前置”)

  • 差异列表(John Hughes列表),具有常量时间追加

  • 支持常数时间追加的代数数据类型:

    data Alist a = ANil | ASingle a | AAppend (Alist a) (Alist a)
    
  • 最终concat列表。

所有这些都需要线性时间。但是恒定因素很重要,而唯一的方法就是建立和衡量。如果您愿意,可以通过将每个列表操作记录到编写器monad中来创建完全忠实于原始代码但仅执行列表操作的微基准测试。但这可能是屁股的巨大痛苦而且不值得。相反,编写一个简单的基准测试,编译(打开优化)和测量。

请告诉我们结果。

答案 1 :(得分:12)

如果订单无关紧要,只需使用普通列表即可。前置(consing)是O(1)并且整个列表的行走是O(n),这与你感兴趣的操作一样好。

如果您实际关心的是附加而不是前置,则差异列表很有用,因为虽然前置对于普通列表来说很快,但是附加是O(n)。差异列表允许O(1)追加。除了易于附加之外,差异列表在每种情况下都比正常列表慢或慢。

答案 2 :(得分:6)

如果你可以逐个追加元素,那么普通列表就可以了。

如果只能附加块,那么列表列表会更好,因为添加新块会变成O(1)而不是O(N),其中N是块大小。

有两个因素可帮助列表快速列出:

  • 懒惰
  • 列表融合

只有当你由一个好的制作人制作一个列表并由一个好的消费者使用它时,两者才会起作用。因此,如果您的producer and consumer are good并且您以单线程方式使用列表,那么由于列表融合,GHC将生成仅循环并且根本没有中间列表。存在两种不同的列表融合实现:所谓的构建/折叠和流融合。另请参阅http://www.haskell.org/haskellwiki/Correctness_of_short_cut_fusion

如果生产者和消费者都很好但列表融合没有参与(因为你没有使用优化标志,因为GHC不支持特定的融合优化,或者如果你使用GHC以外的编译器而没有融合支持),你将会由于懒惰,仍然可以获得合理的表现。在这种情况下,将生成中间列表,但垃圾收集器会立即收集。

答案 3 :(得分:4)

如果通过追加你的意思是“在列表的末尾添加一个元素”,并且你通过xs ++ [x]实现,那么是的,因为每个++都是O,因此对于大型列表来说非常慢n),使总O(n ^ 2)。

在这种情况下,只需使用cons将元素添加到列表的前面而不是结尾,就可以加快速度。这使得构建列表O(n)的整个过程成为可能。然后你可以使用reverse来反转它,也就是O(n),但你只需要做一次,所以你仍然是O(n)。

如果您的处理不受订单影响,或者可以按相反顺序进行稍作修改,那么无论如何都可以忽略reverse。在这种情况下,您还可以利用懒惰来仅在处理元素时构建元素,这意味着您不需要内存中的整个列表,这可能会加快代码的速度,具体取决于代码的内存行为;如果每个列表元素都适合CPU缓存,那么您可以通过这种方式获得大幅加速。

如果通过追加你的意思是“将列表连接到另一个列表的末尾”,你可以通过使用某种“反向前置”操作来做同样的事情,其中​​你将新列表中的元素包含在前面的目标一次列出一个元素;这为您提供了每个新列表的大小而不是您正在构建的列表的线性列表并置,因此它在您处理的元素总数中总体为O(n),而不是O(n ^ 2)。

或者你可以使用cons以相反的顺序建立一个列表列表,然后使用某种反向展平操作处理它,该操作也应该是O(n)。

在这种情况下(多元素追加),除非你的最终处理完全与订单无关,否则仍然很难看到如何完全避免反转。

当然,如果您对高性能的需求不仅仅是避免超线性操作,那么您可能必须完全不同地查看不同的数据结构。

答案 4 :(得分:2)

如果段的长度不同,请考虑列表列表。并concat。懒惰应该应付它。