このソリューションが非常に遅い理由を見つける方法。ほとんどの計算時間が費やされているので、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 div
2 + 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
全体の実行時間は同じですが、コードは少しすっきりしています。
time
DonがTime Profilesで述べたユーティリティは単なるLinuxtime
プログラムです。Windowsでは使用できません。したがって、Windows(実際にはどこでも)での時間プロファイリングについては、この質問を参照してください。