【发布时间】:2019-06-05 22:15:16
【问题描述】:
我正在测试这个递归 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
为什么第一次迭代(以及第二次)需要更多时间,而接下来的迭代速度却快得令人难以置信?
【问题讨论】:
-
一个大组件可能正在缓存;第一次迭代必须实际计算无限列表的前 100000000 个项,但它们会在接下来的迭代中保留在内存中。
-
有些可疑。我使用
takeC (100000000+i)试图避免缓存(没有成功)。使用-O0,我得到了运行在约 5MB 上的慢代码(每个循环约 18 秒)。使用-O2,使用高达 5GB (!!!) 的常驻内存,我得到了更快的循环,如上所示(第一个 10 秒,第二个 0.5 秒)。我不得不使用Int而不是默认的Integer来防止死于OOM。看起来像缓存,但在这种情况下缓存是一个非常错误的优化。 -
您使用的是什么确切的测试程序/编译器标志,以及 GHC 和 Conduit 的哪个版本为您提供了这些数字?使用 GHC 8.6.4(使用
-O2)和 Conduit 1.3.1.1,以及主程序main = evaluate_listC 10,我看到第一次迭代在恒定内存中运行,并在 2 秒内完成,随后的迭代使用记忆值(即,全部在几微秒内运行)。降级到 Conduit 1.3.0(带有Conduit便利模块的最早版本),第一次迭代减慢到 4.2 秒,但我不知道如何让它比这更慢。