Haskellは遅延評価を採用した言語です。第9回では,遅延評価によってプログラムの実行が効率的になる場合がある一方,遅延評価が原因で領域漏れ(スペース・リーク)が発生する可能性があることを説明しました。

 プログラムの中で領域漏れが生じている個所は,一見しただけではなかなかわかりません。そこでGHCなどのHaskell処理系では,ヒープの使用状況を見るためのプロファイラが用意されています。今回は,ヒープ・プロファイラの情報を手がかりにして領域効率の問題がある個所を探す方法を説明します。

 今回の記事には一つ注意点があります。この記事で使用する例や分析は,2011年3月3日にリリースされたGHC 7.0.2でのコンパイル結果に基づくものです。将来のGHCでは,最適化機能やライブラリの改善などによって,異なる結果になる可能性があります。実際にプログラムを効率化する際には,使用する処理系での結果に基づいて分析するよう心がけてください。

プロファイラの基本的な使い方

 まず,GHCのヒープ・プロファイラの使い方を説明しましょう。プロファイルの対象には,第9回で使用した以下のプログラムを利用することにします。

main = print $ sum [0..1000000]

 このプログラムを,前回説明した-profや-auto-all,-caf-allといったオプションを付けてビルドし,RTS(ランタイムシステム)の-h<x>オプションを使って実行します。あえて非効率なプログラムになるように,最適化オプションは外しておきましょう。最適化オプションを使うと,GHCの正格性解析器によって非効率性の問題が解消されてしまいます。

$ ghc  Lazy2.hs -rtsopts -prof -auto-all -caf-all
[1 of 1] Compiling Main             ( Lazy2.hs, Lazy2.o )
Linking Lazy2 ...
$ ./Lazy2 +RTS -hc -K100M
500000500000

 ここでは,-hcオプションを使ってプログラムを実行します。このオプションを指定することで,ヒープ領域上の生存データを作成した主なコスト集約点の結果が生成されます。

 プログラムが終了すると<実行コマンド名>.hpという名前のファイルが生成されます。この場合はLazy2.hpというファイル名になります。以下がそのファイルの内容です。

JOB "Lazy2.exe +RTS -hc -K100M"
DATE "Wed Mar 02 21:41 2011"
SAMPLE_UNIT "seconds"
VALUE_UNIT "bytes"
BEGIN_SAMPLE 0.00
END_SAMPLE 0.00
BEGIN_SAMPLE 0.06
(151)GHC.IO.Handle.FD.CAF       316
(222)CAF:main   56
(228)main/CAF:main      27992012
END_SAMPLE 0.06
BEGIN_SAMPLE 0.20
(151)GHC.IO.Handle.FD.CAF       316
(222)CAF:main   56
(228)main/CAF:main      10692116
END_SAMPLE 0.20
BEGIN_SAMPLE 0.31
END_SAMPLE 0.31

 このファイルをGHCに同梱されているhp2hsコマンドに渡すことで,ヒープ領域のメモリーの使用状況を示すグラフを<実行コマンド名>.psという名前のPostScriptファイルとして作成できます。この場合は,Lazy2.psという名前になります。

$ hp2ps Lazy2.hp

 このグラフには,ヒープ領域の主な生存データ(オブジェクト)の推移が,時間経過に沿って表示されます。作成されるグラフの細かい形は,実行タイミングなどの条件によって微妙に変わります。重要なのは,グラフが示す項目とグラフの大まかな形です。この例では,CAF:mainから呼び出されるmainによって,「メモリーの使用量が16Mバイト程度まで増加し,その後減少する」という三角形のグラフができています。

 グラフの右側にはmain/CAF:mainという項目名が表示されています。これは「CAF:main」および「CAF:mainから呼び出されるmain」を示しており,前回説明した,GHCのプロファイル機能で表示されるコスト集約点と同じものです。

./Lazy2 +RTS -p

   Wed Mar 02 21:45 2011 Time and Allocation Profiling Report  (Final)

           Lazy2.exe +RTS -p -K100M -RTS

        total time  =        0.20 secs   (10 ticks @ 20 ms)
        total alloc = 112,748,580 bytes  (excludes profiling overheads)

~ 略 ~
                                                                                               individual    inherited
COST CENTRE              MODULE                                               no.    entries  %time %alloc   %time %alloc

MAIN                     MAIN                                                   1           0   0.0    0.0   100.0  100.0
 CAF:main                :Main                                                222           1   0.0    0.0     0.0    0.0
 CAF:main                Main                                                 221           1   0.0    0.0   100.0  100.0
  main                   Main                                                 228           1 100.0  100.0   100.0  100.0
~ 略 ~

 コスト集約点は,前回説明したプロファイル機能と同様に,-auto-allオプションや-caf-allオプション,SCC指示文などによって事前に設定します。コスト集約点を作成せずに-hcオプションを使用した場合には,モジュール上のCAFのメモリー使用状況だけが表示されます。

$ ghc  Lazy2.hs -rtsopts -fforce-recomp -prof
[1 of 1] Compiling Main             ( Lazy2.hs, Lazy2.o )
Linking Lazy2 ...
$ ./Lazy2 +RTS -hc -K100M
500000500000
$ hp2ps Lazy2.hp

 なお,データを作成したコスト集約点の情報が不要で,データを作成したモジュールごとのメモリー使用状況だけを表示したい場合には,-hmオプションを使います。

$ ghc  Lazy2.hs -rtsopts -fforce-recomp -prof -auto-all -caf-all
[1 of 1] Compiling Main             ( Lazy2.hs, Lazy2.o )
Linking Lazy2 ...
$ ./Lazy2 +RTS -hm -K100M
500000500000
$ hp2ps Lazy2.hp

 次に,生存データの内容を見てみましょう。生存データに含まれている型を調べるには,-hyオプションを使います。

$ ./Lazy2 +RTS -hy -K100M
500000500000
$ hp2ps Lazy2.hp

 複数の項目があると,モノクロのグラフではそれぞれの項目を区別しにくくなります。そこでhp2psではグラフをカラーで出力するための-cオプションが用意されています(以降は,-cオプションを利用します)。

$ hp2ps -c Lazy2.hp

 このグラフでは,項目名としてInteger型とGHCの内部で使用されているBLACKHOLEという型が表示されています。しかし,いつもこのように型が明確に表示されるとは限りません。

 実は,-hyオプションで表示されるのは,コンパイル時に捨てられる型情報そのものではなく,特定の型を表現するのに使われるクロージャ(closure)です。BLACKHOLEはクロージャとしては個別の型を持ちますが,Haskellのソースコード上ではBlockReason型を表現するデータ構成子の一つに過ぎません(参考リンク)。生存データが関数だったり多相的な型だったりする場合には,使われている型をヒープ・プロファイラが判断できず,型が不明になってしまうことがあります。ヒープ・プロファイラが型を判断できない場合には,実際に使用された型そのものではなく,使用された型を近似する文字列が項目名として使用されます。このプログラムでも,実行タイミングによっては多相型を示す「*」という文字が項目名として表示されることがあります(参考リンク)。

 このような場合には,-hdオプションを使って生存データ内のクロージャの説明(Description)を表示するとよいでしょう。クロージャが実際のデータを示している場合には,データ構成子の名前などが表示されます。そうでない場合には,コンパイラによって生成されたクロージャを示す識別子が出力されます。

$ ./Lazy2 +RTS -hd -K100M
500000500000
$ hp2ps Lazy2.hp

 S#はInteger型を表現するデータ構成子の一つです。先の例の「*」に相当するのが「<base:Data.List.sat_s39p>」というクロージャです。このクロージャだけでは実際の処理まではわかりませんが,Data.Listモジュールから生成されているため,少なくともリスト処理に利用されるものであることはわかります。

 -hyオプションでの「*」や-hdオプションでの「<base:Data.List.sat_s39p>」という項目名は,実行タイミングによっては表示されないことがあります。実行タイミングによる表示結果のブレが大きい場合には,プロファイルの実行間隔をデフォルトの0.1秒よりも短くするとよいでしょう。そのために使うのが-iオプションです。ここでは実行間隔を0.05秒に設定しています。

$ ./Lazy2 +RTS -hd -i0.05 -K100M
500000500000
$ hp2ps Lazy2.hp