5.4. メモリ使用状況のプロファイルを取る

プログラムの時間的、及びメモリ確保の挙動についてプロファイルを取るのに加えて、プログラムのメモリ使用状況の時間的な変化を表したグラフを生成することができる。これは、プログラムが実行時に必要以上のメモリを使っているときにスペースリークの原因を探るのに便利である。スペースリークはGCを酷使するので実行が遅くなりがちであるし、場合によってはプログラムがメモリを使い果たすこともあり得る。

プログラムのヒーププロファイルを生成するには、以下の手順に従う。

  1. プログラムをプロファイル用にコンパイルする。(5.2. プロファイルについてのコンパイルオプション)

  2. 下で解説されているヒーププロファイルオプションのどれかを付けて実行する。(例えば、基本的な生産者プロファイルなら-h)。これでprog.hpというファイルが生成される。

  3. hp2psを実行して、Postscriptファイルであるprog.psを作る。hp2psユーティリティは5.5. hp2ps--ヒーププロファイルをPostScriptへで詳述されている。

  4. できたヒーププロファイルをGhostviewのようなpostscript閲覧器を使って表示するか、Postscript対応のプリンタで印刷する。

例として、これは上の5.1.1. コスト集約点を手動で挿入するで与えたプログラムに対して生成されたヒーププロファイルである。

ヒーププロファイルを表示するためのより高度な道具の集まり(GHCには付属していない)として、hp2anyも見てみると良いかもしれない。

5.4.1. ヒーププロファイルのためのRTSオプション

生成できるヒーププロファイルの種類は複数ある。どの種類でも出力は時間に対する生存ヒープのグラフだが、生存ヒープを分類するときの内訳の取りかたが違う。分類の方法は以下のRTSオプションで選択する。

-hc

(-hに短縮可能)そのデータを生成したコスト集約点スタックで分類する。

-hm

そのデータを生成したコードの所属するモジュールで分類する。

-hd

クロージャの説明で分類する。実際のデータについては、説明とは構築子の名前のことである。それ以外のクロージャについてはそれを識別するコンパイラ生成の文字列である。

-hy

で分類する。型が不明/多相的な関数については、実際の型を近似する文字列が使われる。

-hr

維持原因(retainer)集合で分類する。維持原因プロファイルは下で詳しく解説されている。(5.4.2. 維持原因プロファイル)

-hb

経歴で分類する。経歴プロファイルは以下でより詳しく説明されている。(5.4.3. 経歴プロファイル)

さらにプロファイルは特定の条件を満たすヒープデータに限って行うことができる。例えば、プロファイルを型ごとに表示したいが、これを特定のモジュールで生産されたデータについてのみ行いたい、あるいは、特定の型のデータについて維持原因のプロファイルを行いたい、ということがあるかもしれない。このような制限は以下のように指定できる。

-hcname,...

生産地点において「指定されたコスト集約点のいずれかがスタックの先頭にあった」クロージャのみを対象にする。

-hCname,...

生産地点において「指定されたコスト集約点のいずれかがスタックのどこかにあった」クロージャのみを対象にする。

-hmmodule,...

特定のモジュールで生産されたクロージャのみを対象にする。

-hddesc,...

説明文字列が指定されたものと一致するクロージャのみを対象にする。

-hytype,...

指定された型のクロージャのみを対象にする。

-hrcc,...

維持原因集合の中に「指定されたコスト集約点のいずれかを先頭とするスタックがある」クロージャのみを対象にする。

-hbbio,...

指定された経歴のいずれかに該当するクロージャのみを対象にする。biolagdragvoiduseのいずれかである。

例として、以下のオプションでは、構築子Branch及びLeafに限定した保持原因プロファイルが生成される。

prog +RTS -hr -hdBranch,Leaf

「分類」オプション(上記の例では-hr)は一つしか与えられないが、適用できる制約の数に上限はない。全てのオプションは基本的に組み合わせ可能であるが、例外として、GHCは今のところ-hr-hbオプションを併用するのをサポートしていない。

ヒーププロファイルに関するオプションがあと三つある。

-isecs:

プロファイル間隔(標本取得間隔)をsecs秒(デフォルトは0.1秒)に設定する。小数も使える。例えば-i0.2とすれば毎秒五回標本を取得する。これが影響するのはヒーププロファイルだけである。時間プロファイルでは常にRTS時計に合わせた周期で標本が取得される。これを変えることについては5.3. 時間及び確保量のプロファイルを取るを見よ。

-xt

ヒーププロファイルに、スレッドが占めるメモリを含める。個々のスレッドは、スタック用の空間(スタックは通常小さい状態で開始し、必要に応じて成長する)と、それに加えてスレッド状態のための小さな領域を使う。

これには主スレッドも含まれるので、-xtはプログラムが使っているスタック空間の大きさを知るのに良い。

スレッドが占めるメモリとスタックは、クロージャの説明での分類、型での分類に際しそれぞれ「TSO」「STACK」と表示される。

-Lnum

ヒーププロファイルにおける、コスト集約点スタックの名前の長さの最大値を設定する。デフォルトは25。

5.4.2. 維持原因プロファイル

維持原因プロファイルはなぜこのデータが回収されずに残っているのかという類の疑問に答えるように設計された。まず、維持原因オブジェクトとは何かを定義しよう。

維持原因オブジェクトとは、システムスタックまたは未評価のクロージャ(サンク)または明示的に可変(mutable)なオブジェクトである。

特に、構築子は維持原因オブジェクトではない

オブジェクトBがオブジェクトAの維持原因であるのは、(i)Bが維持原因オブジェクトであって、(ii)Bから始めてポインタを再帰的にたどることで、途中で他の維持原因オブジェクトに出会うことなくAに到達できる場合である。全ての生存オブジェクトは一つ以上の維持原因によって維持されているが、これらの維持原因を総称して、そのオブジェクトの維持原因集合、または維持原因集合、または維持原因たちと呼ぶ。

プログラムに-hrオプションが与えられ、維持原因プロファイルが要請されると、維持原因集合で分類されたグラフが生成される。維持原因集合はコスト集約点スタックの集合として表示される。通常これはプロファイルグラフに載せるには大きすぎるので、維持原因集合には一つずつ番号が振られ、グラフ中では番号つきで短縮して表示される。そして、維持原因集合の完全な一覧はprog.profというファイルに出力される。

維持原因プロファイルでは、全てのオブジェクトについて維持原因集合を得るために生存ヒープを複数回走査しなければならず、これは場合によってはとても遅い。このため、維持原因集合の最大サイズが設定されていて、これよりも大きな維持原因集合はMANYという特殊な集合になる。この最大サイズはデフォルトでは8であり、RTSオプション-Rで変更できる。

-R size

維持原因集合の要素数をsizeに制限する。(デフォルト: 8)

5.4.2.1. 維持原因プロファイルに関するヒント

維持原因オブジェクトの定義は、スペースリークの良くある原因を反映するようにしてある。つまり、大きな構造がある未評価の計算によって保持され、その計算が実行され次第開放される、という状況である。一つの好例として、有限写像から値を見つけ出す(lookup)操作がある。このlookup操作がタイミング良く実行されないと、未評価のlookup操作が写像全体を生き長らえさせることになる。この種のスペースリークは、seqやデータ構築子のフィールドの正確性注釈を使って、関連する計算を強制することで防げることが多い。

特定のデータ構造が一連の未評価のクロージャの列によって保持されているということが良くある。この場合、最も近いものだけが維持原因プロファイルで報告される。例えば、AがBを保持し、BがCを保持し、Cが大きな構造を保持しているとしよう。さらに、Bはたくさんあるが、Aは一つしかなく、従ってAを排除したいということがあるかもしれない。しかし、保持原因プロファイルはこの場合大きな構造の保持原因としてBを挙げる。そこで、この列を一つたどるために、Bのオブジェクトに絞ってもう一回維持原因プロファイルを取ることができる。こうすることで、Bの維持原因プロファイルが得られる。

prog +RTS -hr -hcB

この技は完璧ではない。無関係なBのクロージャがヒープ中にあるかもしれないからである。しかし、これで大抵の場合うまく行くということが判っている。

5.4.3. 経歴プロファイル

典型的なヒープオブジェクトは、生存期間中の各時点において以下のいずれかの状態をとる。

  • lag(待機)段階。作られてから最初に使われるまでの間。

  • use(使用中)段階。最初に使われてから最後に使われるまでの間。

  • drag(惰性)段階。最後に使われてから参照されなくなるまでの間。

  • 一回も使われないオブジェクトについては、生存期間中常にvoid(空虚)状態にあるとみなされる。

経歴ヒーププロファイルでは、上記の四状態にある生存ヒープの割合を表示する。通常、最も重要なのはvoid状態とdrag状態である。これらの状態にある生存ヒープはlag状態やuse状態にあるヒープよりも無駄である可能性が高い。

これらのうち一つまたは複数の状態にあるものについて、別の基準で分類することもできる。これには、プロファイルを経歴で制限すれば良い。例えば、drag状態またはvoid状態にあるヒープについて、割合を生産者別で表示するなら、以下のようにすれば良い。

prog +RTS -hc -hbdrag,void

drag状態やvoid状態にあるヒープの生産者や型が分かったら、次にすることは維持原因を見つけることだろう。

prog +RTS -hr -hccc...

注意: このように二段階に分けて処理しないといけないのは、現在GHCがプロファイルを取るときに経歴と維持原因の両方を同時に使うことができないからである。

5.4.4. 実際のメモリ使用量

ヒーププロファイルで報告されるメモリ使用量と、プログラムを実行したときの実際のメモリ使用量とはどう関係しているか。ヒーププロファイルで報告されるメモリ使用量と、システムのツール(Unixならpstop、Windowsならタスクマネージャなど)で報告されるメモリ使用量との間に大きな差があるのを目にするかも知れない。これにはいくつかの原因がある。

  • プロファイル自体にオーバーヘッドがあるが、プロファイル時のメモリ使用量の数値からは引かれている。もちろん、このオーバーヘッドはプロファイルのサポートなしでコンパイルすれば消滅する。現在、空間オーバーヘッドはヒープオブジェクト一つあたり二ワードであり、この結果30%程度のオーバーヘッドになる。

  • GCには実際の使用量よりもたくさんのメモリが必要である。この比は使われているGCのアルゴリズムに依存する。標準である世代別コピーGCでは、Lを生存データの量として通常3Lバイトのメモリを必要とする。これは、デフォルトでは(+RTS -Fオプションを見よ)古い世代が回収前の時点で二倍(2L)になり得、さらにコピー先としてLバイトの空間が必要だからである。コンパクト化GC(+RTS -cオプションを見よ)を使うときは、これは2Lに減り、-Fを調整することでさらに減らせる。また、確保領域の大きさも加わる。(現在は512kbに固定されている)

  • デフォルトではヒーププロファイルにはスタックが含まれない。+RTS -xtオプションを見よ。

  • プログラムテキスト自体、Cスタック、あらゆるヒープ外データ(例えば、外部のライブラリで確保されたデータやRTSが確保したデータ)、mmap()されたメモリはヒーププロファイルで考慮されない。