如何找出这个解决方案如此缓慢的原因。是否有任何命令可以告诉我大部分计算时间都花在了哪里,以便我知道我的 haskell 程序的哪一部分很慢?
没错! GHC 提供了许多优秀的工具,包括:
使用时间和空间分析的教程是part of Real World Haskell。
GC 统计数据
首先,确保您使用 ghc -O2 进行编译。您可以确保它是现代 GHC(例如 GHC 6.12.x)
我们可以做的第一件事是检查垃圾收集是否不是问题。
用 +RTS -s 运行你的程序
$ time ./A +RTS -s
./A +RTS -s
749700
9,961,432,992 bytes allocated in the heap
2,463,072 bytes copied during GC
29,200 bytes maximum residency (1 sample(s))
187,336 bytes maximum slop
**2 MB** total memory in use (0 MB lost due to fragmentation)
Generation 0: 19002 collections, 0 parallel, 0.11s, 0.15s elapsed
Generation 1: 1 collections, 0 parallel, 0.00s, 0.00s elapsed
INIT time 0.00s ( 0.00s elapsed)
MUT time 13.15s ( 13.32s elapsed)
GC time 0.11s ( 0.15s elapsed)
RP time 0.00s ( 0.00s elapsed)
PROF time 0.00s ( 0.00s elapsed)
EXIT time 0.00s ( 0.00s elapsed)
Total time 13.26s ( 13.47s elapsed)
%GC time **0.8%** (1.1% elapsed)
Alloc rate 757,764,753 bytes per MUT second
Productivity 99.2% of total user, 97.6% of total elapsed
./A +RTS -s 13.26s user 0.05s system 98% cpu 13.479 total
这已经给了我们很多信息:你只有一个 2M 的堆,而 GC 占用了 0.8% 的时间。所以不用担心分配是问题。
时间档案
为您的程序获取时间配置文件很简单:使用 -prof -auto-all 进行编译
$ ghc -O2 --make A.hs -prof -auto-all
[1 of 1] Compiling Main ( A.hs, A.o )
Linking A ...
并且,对于 N=200:
$ time ./A +RTS -p
749700
./A +RTS -p 13.23s user 0.06s system 98% cpu 13.547 total
创建一个文件 A.prof,其中包含:
Sun Jul 18 10:08 2010 Time and Allocation Profiling Report (Final)
A +RTS -p -RTS
total time = 13.18 secs (659 ticks @ 20 ms)
total alloc = 4,904,116,696 bytes (excludes profiling overheads)
COST CENTRE MODULE %time %alloc
numDivs Main 100.0 100.0
表示所有你的时间都花在了numDivs上,也是你所有分配的来源。
堆配置文件
您还可以通过运行 +RTS -p -hy 来分解这些分配,这会创建 A.hp,您可以通过将其转换为 postscript 文件 (hp2ps -c A.hp) 来查看它,生成:
这告诉我们您的内存使用没有任何问题:它在恒定空间中分配。
所以你的问题是 numDivs 的算法复杂性:
toInteger $ length [ x | x<-[2.. ((n `quot` 2)+1)], n `rem` x == 0] + 2
解决这个问题,这是您运行时间的 100%,其他一切都很容易。
优化
这个表达式是stream fusion优化的一个很好的候选,所以我会重写它
使用Data.Vector,像这样:
numDivs n = fromIntegral $
2 + (U.length $
U.filter (\x -> fromIntegral n `rem` x == 0) $
(U.enumFromN 2 ((fromIntegral n `div` 2) + 1) :: U.Vector Int))
应该融合成一个没有不必要的堆分配的循环。也就是说,它将比列表版本具有更好的复杂性(通过常数因素)。您可以使用 ghc-core 工具(高级用户)检查优化后的中间代码。
对此进行测试,ghc -O2 --make Z.hs
$ time ./Z
749700
./Z 3.73s user 0.01s system 99% cpu 3.753 total
因此它在不改变算法本身的情况下将 N=150 的运行时间减少了 3.5 倍。
结论
您的问题是 numDivs。它是您运行时间的 100%,并且具有可怕的复杂性。 想想 numDivs,例如,对于每个 N,您如何生成 [2 .. n div2 + 1] N 次。
尝试记住它,因为值不会改变。
要衡量您的哪个函数更快,请考虑使用criterion,它将提供有关运行时间亚微秒改进的统计可靠信息。
附录
由于 numDivs 是您运行时间的 100%,因此接触程序的其他部分不会有太大区别,
但是,出于教学目的,我们也可以使用流融合重写那些。
我们也可以重写trialList,依靠fusion变成你在trialList2中手写的循环,
这是一个“前缀扫描”功能(又名 scanl):
triaList = U.scanl (+) 0 (U.enumFrom 1 top)
where
top = 10^6
同样适用于 sol:
sol :: Int -> Int
sol n = U.head $ U.filter (\x -> numDivs x > n) triaList
总体运行时间相同,但代码更简洁。