目次
Glasgow Haskellには時間及び空間のプロファイルをとるためのシステムが付属している。このシステムの目的は、プログラムの実行時の振る舞いをより良く理解できるようにし、ひいてはそれを改善できるようにすることである。
あらゆる意見、提案、改善は歓迎される。おすすめの「プロファイル技」があるなら特に素晴らしい。
プログラムのプロファイルをとるのは三つの段階からなる。
プログラムをプロファイル用にコンパイルする。これには、-prof
オプションと、多くの場合-auto
と-auto-all
オプションのいずれかを付ける。これらのオプションは5.2. プロファイルについてのコンパイルオプションでさらに詳しく解説されている。
プロファイルオプションのどれか(例えば+RTS -p -RTS
)を付けてプログラムを実行する。プロファイル情報のファイルが生成される。プロファイル中は複数プロセッサによる実行(例えば+RTS -N2
)に対応していないことに注意。
生成されたプロファイル情報を調べる。これにはGHCのプロファイルツールを使う。どれを使うかは生成されたプロファイル情報によって異なる。
GHCのプロファイルシステムではコストはコスト集約点(cost centre)に割り当てられる。コストとは式を評価するのに必要な時間と空間のことである。コスト集約点はプログラム上の注釈で、一定の範囲の式を支配する。注釈の付いた式が発生させたコストは全て、それを直接支配するコスト集約点に割り当てられる。さらに、GHCは任意の式についてそれを支配するコスト集約点のスタックを実行時に記憶していて、どこにどれだけコストが掛かったかという情報の付いた呼び出しグラフを生成する。
例を一つ見てみよう。
main = print (nfib 25) nfib n = if n < 2 then 1 else nfib (n-1) + nfib (n-2)
このプログラムを次のようにコンパイルし、実行する。
$ ghc -prof -auto-all -o Main Main.hs $ ./Main +RTS -p 121393 $
GHCでコンパイルされたプログラムは、-p
というRTSオプション付きで実行されると、<prog>.prof
というファイルを生成する。この場合、ファイルの内容は以下のようなものである。
Fri May 12 14:06 2000 Time and Allocation Profiling Report (Final) Main +RTS -p -RTS total time = 0.14 secs (7 ticks @ 20 ms) total alloc = 8,741,204 bytes (excludes profiling overheads) COST CENTRE MODULE %time %alloc nfib Main 100.0 100.0 individual inherited COST CENTRE MODULE entries %time %alloc %time %alloc MAIN MAIN 0 0.0 0.0 100.0 100.0 main Main 0 0.0 0.0 0.0 0.0 CAF PrelHandle 3 0.0 0.0 0.0 0.0 CAF PrelAddr 1 0.0 0.0 0.0 0.0 CAF Main 6 0.0 0.0 100.0 100.0 main Main 1 0.0 0.0 100.0 100.0 nfib Main 242785 100.0 100.0 100.0 100.0
ファイルの最初の部分は、プログラムの名前、オプション、実行時に計測された合計実行時間と合計メモリ確保量を示している。(合計メモリ確保量はある一つの時点においてプログラムが必要とする生存メモリの量とは異なることに注意。後者はヒーププロファイルで量れるが、これについては後で説明する)
ファイルの二番目の部分は、プログラムの中でコストが高い関数をコスト集約点で分類したものである。この例では、プログラムにはコストの高い関数が一つ(nfib
)しかなく、これがプログラムの時間と確保量の両方について100%のコストを占めている。
三番目の最後の節はコスト集約点スタックで分類されたプロファイルを表示している。これはプログラムの呼び出しグラフとだいたい同じである。上記の例では、コストの高いnfib
の呼び出しがmain
由来のものであることが明らかになっている。
プログラムの特定の部分の時間と確保量は二種類が表示されている。「individual」はこのコスト集約点スタックに相当する部分のコードが消費したものだけを示す。「inherited」はこのノードの子が消費したものも全て含む。
例を少し変えると、コスト集約点スタックの有用性がより良く分かるようになる。
main = print (f 25 + g 25) f n = nfib n g n = nfib (n `div` 2) nfib n = if n < 2 then 1 else nfib (n-1) + nfib (n-2)
このプログラムを前と同じようにコンパイル・実行し、新しいプロファイル結果を見てみる。
COST CENTRE MODULE scc %time %alloc %time %alloc MAIN MAIN 0 0.0 0.0 100.0 100.0 main Main 0 0.0 0.0 0.0 0.0 CAF PrelHandle 3 0.0 0.0 0.0 0.0 CAF PrelAddr 1 0.0 0.0 0.0 0.0 CAF Main 9 0.0 0.0 100.0 100.0 main Main 1 0.0 0.0 100.0 100.0 g Main 1 0.0 0.0 0.0 0.2 nfib Main 465 0.0 0.2 0.0 0.2 f Main 1 0.0 0.0 100.0 99.8 nfib Main 242785 100.0 99.8 100.0 99.8
nfib
の呼び出しをプログラム中で二回行ったが、時間を食っているのがf
経由の呼び出しだということが明らかだ。
出力の各列の実際の意味は以下の通りである。
呼び出しグラフのこの場所に入った回数。
呼び出しグラフ中のこの場所で消費された時間の、総時間に対する割合。
この呼び出しでなされたメモリ確保量(プロファイルによる余分は除く)の全体に占める割合。
呼び出しグラフ中のこの点以下で消費された時間の、プログラムの総実行時間に占める割合。
この呼び出しとその部分呼び出しでなされたメモリ確保量(プロファイルによる余分は除く)の全体に占める割合。
加えて、RTSオプション-P
を使うと、下記の情報が追加される。
ticks
この集約点に割り当てられた生の時刻信号(tick)の数。上記の%time
はこの値から得られている。
bytes
この集約点の支配下でヒープ中に確保されたバイト数。これは上記の%alloc
の数値の元となるものである。
再帰的関数や相互に再帰的な一群の関数についてはどうだろうか。コストはどこに割り当てられるのか。答えはこうである。GHCは一群の関数が互いに再帰的に呼び合ったという情報は保持するが、時刻と確保の基本プロファイルにおいてはこの情報は表示されず、呼び出しグラフは平坦化されて木として表示される。
コスト集約点は、単なるプログラム上の注釈である。コンパイラに-auto-all
を指示すると、INLINE指定されていない全ての最上位の関数の周りに自動的にコスト集約点が挿入される。しかし、自分でコスト集約点を挿入するのも完全に自由である。
コスト集約点の注釈の構文は以下である。
{-# SCC "name" #-} <expression>
ここで、"name"
は任意の文字列であり、これがこのコスト集約点の名前としてプロファイル出力に現れる。<expression>
は任意のHaskellの式である。パース時にはSCC
注釈は右側に可能な限り長く続くように解釈される。(SCCは「Set Cost Centre」(コスト集約点を設定せよ)の意である)
SCCをいくつか使ったプログラムの例を示す。
main :: IO () main = do let xs = {-# SCC "X" #-} [1..1000000] let ys = {-# SCC "Y" #-} [1..2000000] print $ last xs print $ last $ init xs print $ last ys print $ last $ init ys
実行すると、次のようなヒーププロファイルが得られる。
プログラム中のそれぞれの式を評価するときに発生したコストは以下の規則にしたがってコスト集約点スタックに配分される。
その式が、最上位の定義を評価するときの一回限りのコストの一部であるなら、コストが配分されるのは、その式が字句的に囲まれているSCC
注釈のスタックを、CAF
という特殊なコスト集約点の上に載せたものになる。
そうでない場合、コストが配分されるのは、その式が字句的に囲まれているSCC
のスタックに、呼び出し地点[10]でのコスト集約点スタックを連結したものになる。これが再帰的な定義であることに注意せよ。
他言語のコード(第8章. 他言語関数インタフェース(FFI)を見よ)で経過した時間は常に、その他言語関数をHaskellから呼び出している地点でのコスト集約点に配分される。
一回限りのコストとは何か。Haskellは遅延言語であり、ある種の式はただ一回しか評価されない。例えば、次のように書いたとする。
x = nfib 25
するとx
は(もし評価されるなら)ただ一回だけ評価され、それ以降x
が必要になったときはキャッシュされた結果を使う。x
のような引数のない定義はCAF(Constant Applicative Form; 定作用形)と呼ばれる。
プロファイルについて話すときは、「式nfib 25
はx
を評価するときの一回限りのコストに属している」という。
一回限りのコストは厳密にはプログラムの呼び出しグラフの一部ではないので、特別な最上位のコスト集約点であるCAF
に割り当てられる。CAF
コスト集約点はモジュールに一つ(デフォルト)か、一回限りのコストのある全ての最上位の定義ごとに一つ(-caf-all
をGHCに渡すことでこの挙動になる)ある。
プロファイルが変だと思ったときや、呼び出しグラフが思ったようになっていないときは、遠慮せずにそれ(とプログラム)を<glasgow-haskell-bugs@haskell.org>
から我々に送ってほしい。
呼び出し地点とは、その特定の関数や変数に言及している、ソースコード中の位置のことである。