分析Haskell程序性能的工具


104

在解决一些Euler项目问题​​以学习Haskell时(目前我是一个完全的初学者),我遇到了问题12。我写了这个(幼稚的)解决方案:

--Get Number of Divisors of n
numDivs :: Integer -> Integer
numDivs n = toInteger $ length [ x | x<-[2.. ((n `quot` 2)+1)], n `rem` x == 0] + 2

--Generate a List of Triangular Values
triaList :: [Integer]
triaList =  [foldr (+) 0 [1..n] | n <- [1..]]

--The same recursive
triaList2 = go 0 1
  where go cs n = (cs+n):go (cs+n) (n+1)

--Finds the first triangular Value with more than n Divisors
sol :: Integer -> Integer
sol n = head $ filter (\x -> numDivs(x)>n) triaList2

该解决方案的n=500 (sol 500)运行速度非常慢(现在已经运行了2个小时以上),因此我想知道如何找出为什么该解决方案如此之慢。是否有任何命令告诉我大部分的计算时间花在哪里,所以我知道haskell程序的哪一部分运行缓慢?像一个简单的探查器。

为了明确起见,我并不是在寻求更快的解决方案,而是在寻找一种找到该解决方案的方法。如果您没有Haskell知识,您将如何开始?

我试图编写两个triaList函数,但是找不到方法来测试哪个更快,所以这就是我的问题所在。

谢谢

Answers:


187

如何找出这种解决方案为何如此缓慢的原因。是否有任何命令告诉我大部分计算时间都花在哪里,所以我知道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个生成divN次的[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唐在“时间档案”中提到的实用程序只是Linux time程序。在Windows中不可用。因此,对于Windows上的时间分析(实际上是在任何地方),请参阅问题。
约翰·雷德

1
对于将来的用户,-auto-all不推荐使用-fprof-auto
B. Mehta,

60

通过直接解决问题,Dons的答案很好,但又不会破坏自己。
在这里,我想建议我最近写的一个小工具。当您需要比默认配置文件更详细的配置文件时,可以节省手动编写SCC注释的时间ghc -prof -auto-all。除此之外,它还丰富多彩!

这是您提供的代码(*)的示例,绿色可以,红色很慢: 替代文字

所有时间都在创建除数列表中。这建议您可以执行以下操作:
1. n rem x == 0加快过滤速度,但是由于它是内置函数,可能已经非常快了。
2.创建一个较短的列表。您只检查了,就已经朝着这个方向做了一些事情n quot 2
3.完全舍弃列表生成,并使用一些数学方法以获得更快的解决方案。这是解决项目欧拉问题的常用方法。

(*)我是通过将您的代码放入一个名为eu13.hsmain 的文件中来实现的main = print $ sol 90。然后运行visual-prof -px eu13.hs eu13,结果为eu13.hs.html


3

与Haskell相关的注释:triaList2当然比triaList因为后者执行了大量不必要的计算而速度更快。计算n的前n个元素将花费二次时间triaList,而对于则需要线性时间triaList2。还有另一种优雅(有效)的方法来定义三角形的无限懒惰列表:

triaList = 1 : zipWith (+) triaList [2..]

与数学相关的注释:无需检查所有除数最大为n / 2的数,这就足以检查最多sqrt(n)的数。


2
还请考虑:scanl(+)1 [2 ..]
唐·斯图尔特

1

您可以使用标志运行程序以启用时间分析。像这样:

./program +RTS -P -sprogram.stats -RTS

那应该运行该程序并产生一个名为program.stats的文件,该文件将在每个函数上花费多少时间。您可以在GHC 用户指南中找到有关使用GHC进行性能分析的更多信息。对于基准测试,有Criterion库。我发现这篇博客文章有一个有用的介绍。


1
但首先使用ghc -prof -auto-all -fforce-recomp --make -O2 program.hs
Daniel Velkov进行
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.