用于分析Haskell程序性能的工具

时间:2010-07-18 16:09:31

标签: haskell performance profiling

在解决一些项目Euler问题以学习Haskell(所以目前我是一个完全初学者)时,我来到了Problem 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程序的哪一部分很慢?像一个简单的探查器。

为了说清楚,我不是要求 更快的解决方案,而是一种方式来找到这个解决方案。如果你没有哈斯克尔知识,你会怎么开始?

我尝试编写两个triaList函数,但没有办法测试哪一个更快,所以这就是我的问题开始的地方。

由于

4 个答案:

答案 0 :(得分:181)

  

如何找出解决方案为何如此缓慢。是否有任何命令告诉我大部分计算时间花在哪里,所以我知道我的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)来查看,生成:

alt text

告诉我们你的内存使用没有问题:它是在恒定空间中分配。

所以你的问题是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 - 制作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 div 2 + 1] N次的方式。 请记住,因为值不会改变。

要测量哪些功能更快,请考虑使用criterion,这将提供有关运行时间亚微秒改进的统计信息。


<强>附加物

由于numDivs是您运行时间的100%,因此触摸程序的其他部分不会产生太大影响, 但是,出于教学目的,我们也可以使用流融合重写那些。

我们还可以重写trialList,并依靠fusion将其转换为您在trialList2中手动编写的循环, 这是一个“前缀扫描”功能(aka 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

具有相同的整体运行时间,但代码更清晰。

答案 1 :(得分:59)

通过直接解决问题,Dons的答案很棒而且不会成为扰流板 在这里,我想建议我最近写的一点tool。当您需要比默认ghc -prof -auto-all更详细的配置文件时,它可以节省您手动编写SCC注释的时间。除此之外它很丰富多彩!

以下是您给出的代码示例(*),绿色正常,红色慢: alt text

一直在创建除数列表。这表明你可以做的一些事情:
 1.使过滤n rem x == 0更快,但由于它是内置函数,可能已经很快了  2.创建一个较短的列表。你已经在这个方向做了一些事情,只检查n quot 2  3.完全丢弃列表生成并使用一些数学来获得更快的解决方案。这是项目欧拉问题的常用方法。

(*)我把你的代码放在一个名为eu13.hs的文件中,添加了一个主函数main = print $ sol 90。然后运行visual-prof -px eu13.hs eu13,结果位于eu13.hs.html

答案 2 :(得分:3)

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

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

数学相关注释:没有必要检查最多n / 2的所有除数,它足以检查到sqrt(n)。

答案 3 :(得分:1)

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

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

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