导管管道执行时间奇怪

时间:2019-06-05 22:15:16

标签: haskell conduit

我正在测试此递归Haskell函数的性能,该函数重复求和无限列表的前100000000个整数(使用Conduit管道),并打印每次执行的经过时间:

import Conduit
import Data.Time.Clock

evaluate_listC 0 = return ()
evaluate_listC i = do
    startTime <- getCurrentTime
    print $ runConduitPure $ yieldMany [1..] .| takeC 100000000 .| sumC
    endTime <- getCurrentTime
    print $ diffUTCTime endTime startTime
    evaluate_listC (i-1)

编译(带有-O标志)并运行代码,并对该函数进行10次迭代,我获得以下执行时间:

38.2066878s
4.3696857s
1.3367605s
0.9950032s
0.9399968s
0.9039936s
0.9079987s
0.9119587s
0.9090151s
0.8749654s

为什么第一次迭代(以及第二次迭代)要花更多的时间,而随后的迭代却要快得多?

1 个答案:

答案 0 :(得分:1)

正如我在评论中提到的那样,我无法复制这些性能缓慢的数字,但是我很确定自己知道发生了什么。如果您提供一些其他细节以使我能够重复问题,则可以更新答案。

很有可能,列表[1..](或可能涉及该列表的一些较大表达)作为一种恒定的应用形式(CAF)被“提升”到顶层。由于列表是在第一次迭代中生成的,因此将其保留为“永久”堆对象,以备将来迭代使用。

第一次迭代在 part 中花费很长时间,因为它正在分配和生成列表,尽管由于GHC的“凹凸分配器”,分配非常快,实际上生成列表可能只需要一个几秒钟。大多数时间可能是花费在垃圾收集上。 GC时间与需要从凹凸分配器中恢复(复制)的“重要”内容的大小成比例,您将在此处构建一个庞大的永久列表。

以后的迭代要快得多,因为它们可以在现有列表上运行管道总和。这可能涉及对中间结果的一些分配,但大多数结果并不会停留,因此GC的内容少得多,并且迭代速度很快。

第二次和第三次迭代比后来的迭代慢一点的原因与GHC的世代垃圾收集器有关。最初,永久大列表和其他半永久(例如,仅需要短时间或当前迭代所需的)堆对象都从凹凸分配器中复制出来。进一步的垃圾收集涉及重新复制相同的永久列表,同时允许收集过期的半永久对象。最终,列表被提升到下一代,而所有非列表对象都保留在第一代。

一旦永久性列表和半永久性“其他对象”已完全分离为不同的世代,则在第一代GC期间就不再需要复制该列表,并且迭代时间减少到大约一秒。 >