第5章プロファイルを取る

目次

5.1. コスト集約点とコスト集約点スタック
5.1.1. コスト集約点を手動で挿入する
5.1.2. コストの配分規則
5.2. プロファイルについてのコンパイルオプション
5.3. 時間及び確保量のプロファイルを取る
5.4. メモリ使用状況のプロファイルを取る
5.4.1. ヒーププロファイルのためのRTSオプション
5.4.2. 維持原因プロファイル
5.4.2.1. 維持原因プロファイルに関するヒント
5.4.3. 経歴プロファイル
5.4.4. 実際のメモリ使用量
5.5. hp2ps––ヒーププロファイルをPostScriptへ
5.5.1. hpファイルを操作する
5.5.2. プロファイルの特定の部分に注目する
5.5.3. 実行中のプログラムのヒーププロファイルを見る
5.5.4. ヒーププロファイルを実時間で閲覧する
5.6. コード網羅率を観察する
5.6.1. 小さな例: 逆数をとる
5.6.2. 網羅率の測定器を付与するオプション
5.6.3. hpcツールキット
5.6.3.1. hpc report
5.6.3.2. hpc markup
5.6.3.3. hpc sum
5.6.3.4. hpc combine
5.6.3.5. hpc map
5.6.3.6. hpc overlayとhpc draft
5.6.4. Haskell Program Coverageの注意点と短所
5.7. 「ticky-ticky」プロファイルを使う

Glasgow Haskellには時間及び空間のプロファイルをとるためのシステムが付属している。このシステムの目的は、プログラムの実行時の振る舞いをより良く理解できるようにし、ひいてはそれを改善できるようにすることである。

あらゆる意見、提案、改善は歓迎される。おすすめの「プロファイル技」があるなら特に素晴らしい。

プログラムのプロファイルをとるのは三つの段階からなる。

  1. プログラムをプロファイル用にコンパイルする。これには、-profオプションと、多くの場合-auto-auto-allオプションのいずれかを付ける。これらのオプションは5.2. プロファイルについてのコンパイルオプションでさらに詳しく解説されている。

  2. プロファイルオプションのどれか(例えば+RTS -p -RTS)を付けてプログラムを実行する。プロファイル情報のファイルが生成される。プロファイル中は複数プロセッサによる実行(例えば+RTS -N2)に対応していないことに注意。

  3. 生成されたプロファイル情報を調べる。これには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経由の呼び出しだということが明らかだ。

出力の各列の実際の意味は以下の通りである。

entries

呼び出しグラフのこの場所に入った回数。

individual %time

呼び出しグラフ中のこの場所で消費された時間の、総時間に対する割合。

individual %alloc

この呼び出しでなされたメモリ確保量(プロファイルによる余分は除く)の全体に占める割合。

inherited %time

呼び出しグラフ中のこの点以下で消費された時間の、プログラムの総実行時間に占める割合。

inherited %alloc

この呼び出しとその部分呼び出しでなされたメモリ確保量(プロファイルによる余分は除く)の全体に占める割合。

加えて、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 25xを評価するときの一回限りのコストに属している」という。

一回限りのコストは厳密にはプログラムの呼び出しグラフの一部ではないので、特別な最上位のコスト集約点であるCAFに割り当てられる。CAFコスト集約点はモジュールに一つ(デフォルト)か、一回限りのコストのある全ての最上位の定義ごとに一つ(-caf-allをGHCに渡すことでこの挙動になる)ある。

プロファイルが変だと思ったときや、呼び出しグラフが思ったようになっていないときは、遠慮せずにそれ(とプログラム)をから我々に送ってほしい。

[10]

呼び出し地点とは、その特定の関数や変数に言及している、ソースコード中の位置のことである。