Haskellプログラムのパフォーマンスを分析するためのツール


104

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の知識がない場合、どのように始めますか?

2つのtriaList関数を作成しようとしましたが、どちらが速いかをテストする方法が見つからなかったため、ここから問題が始まります。

ありがとう

回答:


187

このソリューションが非常に遅い理由を見つける方法。ほとんどの計算時間が費やされているので、haskellプログラムのどの部分が遅いのかがわかるコマンドはありますか?

正確に!GHCは、以下を含む多くの優れたツールを提供します。

時間と空間のプロファイリングの使用に関するチュートリアルは、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が作成されます。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-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回生成する方法を考えてください。値は変わらないので、覚えておいてください。

どの関数がより速いかを測定するには、実行時間のサブマイクロ秒の改善に関する統計的に堅牢な情報を提供する基準の使用を検討してください。


補遺

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

全体の実行時間は同じですが、コードは少しすっきりしています。


私のような他のばか者へのメモ:timeDonがTime Profilesで述べたユーティリティは単なるLinux timeプログラムです。Windowsでは使用できません。したがって、Windows(実際にはどこでも)での時間プロファイリングについては、この質問を参照してください。
John Red、

1
将来のユーザーのために、-auto-all非推奨です-fprof-auto
B. Mehta

60

Donsの答えは、問題を直接解決することにより、ネタバレにならずに素晴らしいものです。
ここで私が最近書いた小さなツールを提案したいと思います。デフォルトよりも詳細なプロファイルが必要な場合は、SCCアノテーションを手動で作成する時間を節約できますghc -prof -auto-all。その上、それはカラフルです!

ここにあなたが与えたコードの例があります(*)、緑はOK、赤は遅いです: 代替テキスト

除数のリストの作成には常に時間がかかります。これは、あなたができるいくつかのことを示唆しています:
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ます。


3

Haskell関連の注記:triaList2もちろんtriaList、後者は多くの不要な計算を実行するためよりも高速です。の最初のn個の要素を計算するには2次時間がかかりますがtriaList、では線形ですtriaList2。三角形番号の無限遅延リストを定義するためのもう1つのエレガントな(そして効率的な)方法があります。

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

数学関連の注意:n / 2までのすべての除数をチェックする必要はありません。sqrt(n)までチェックすれば十分です。


2
次も考慮してください:scanl(+)1 [2 ..]
Don Stewart

1

フラグを使用してプログラムを実行し、時間プロファイリングを有効にすることができます。このようなもの:

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

これでプログラムが実行され、program.statsというファイルが生成されます。このファイルには、各関数で費やされた時間が含まれています。GHCを使用したプロファイリングの詳細については、GHC ユーザーガイドを参照してください。ベンチマーク用に、Criterionライブラリがあります。このブログ投稿に役立つ紹介があることがわかりました。


1
しかし、最初にそれをコンパイルしますghc -prof -auto-all -fforce-recomp --make -O2 program.hs
Daniel
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.