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プログラムのどの部分が遅いかがわかりますか?単純なプロファイラーのようなもの。
明確にするために、forより速い解決策を求めているのではなく、wayこの解決策を見つけます。ハスケルの知識がない場合、どのように始めますか?
2つのtriaList
関数を記述しようとしましたが、どちらが高速かをテストする方法が見つからなかったため、ここから問題が始まります。
ありがとう
このソリューションが非常に遅い理由を見つける方法。ほとんどの計算時間が費やされる場所を教えてくれるコマンドがありますので、haskell-programのどの部分が遅いのかがわかりますか?
正確に! 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
allあなたの時間はnumDivsに費やされ、すべての割り当てのソースでもあることを示します。
ヒーププロファイル
また、+ RTS -p -hyを実行して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ごとに[2 .. n div
2 + 1]をN回生成する方法を考えてください。値は変わらないので、それを覚えてみてください。
どの関数がより高速かを測定するには、 criterion の使用を検討してください。これにより、実行時間のサブマイクロ秒の改善に関する統計的に堅牢な情報が提供されます。
補遺
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
全体の実行時間は同じですが、コードが少しすっきりしています。
問題の直接的な解決策を提供することで、ネタバレにならずにDonsの答えは素晴らしい。
ここで、最近書いたちょっとした tool を提案したいと思います。デフォルトのghc -prof -auto-all
よりも詳細なプロファイルが必要な場合、SCC注釈を手で書く時間を節約できます。それに加えて、それはカラフルです!
これは、指定したコード(*)の例です。緑は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
になります。
Haskell関連のメモ:triaList2
はもちろんtriaList
よりも高速です。後者は多くの不要な計算を実行するためです。 triaList
のn個の最初の要素を計算するのに2次時間がかかりますが、triaList2
に対して線形です。三角形の数の無限の遅延リストを定義する別のエレガントな(そして効率的な)方法があります:
triaList = 1 : zipWith (+) triaList [2..]
数学関連の注意:n/2までのすべての除数をチェックする必要はありません。sqrt(n)までチェックすれば十分です。