如何找出这种解决方案为何如此缓慢的原因。是否有任何命令告诉我大部分计算时间都花在哪里,所以我知道haskell程序的哪一部分运行缓慢?
恰恰!GHC提供了许多出色的工具,包括:
Real Time 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,然后将其转换为后记文件(hp2ps -c A.hp)进行查看,并生成:
这告诉我们您的内存使用没有问题:它在恒定空间中分配。
因此,您的问题是numDivs的算法复杂度:
toInteger $ length [ x | x<-[2.. ((n `quot` 2)+1)], n `rem` x == 0] + 2
修复此问题,这是您的运行时间的100%,其他所有操作都很简单。
最佳化
该表达式是流融合优化的理想选择,因此我将其重写为使用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个生成div
N次的[2 .. n 2 +1]。尝试记住这一点,因为值不会改变。
要衡量哪个功能更快,请考虑使用criteria,它将提供有关运行时间亚微秒改进的统计上可靠的信息。
附加物
由于numDivs是您运行时间的100%,因此触摸程序的其他部分不会有多大区别,但是,出于教学目的,我们还可以使用流融合来重写它们。
我们还可以重写trialList,并依靠融合将其转变为您在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
总体运行时间相同,但是代码更简洁。
time
唐在“时间档案”中提到的实用程序只是Linuxtime
程序。在Windows中不可用。因此,对于Windows上的时间分析(实际上是在任何地方),请参阅此问题。