Space leak zooを翻訳しました。
翻訳に間違いがあったり、誤解を生みそうな表現があった場合はコメントや編集リクエストなどで指摘していただけると助かります。
スペースリーク動物園
まず、スペースリークの標本集に情報を提供してくれた全ての人に感謝します。
みなさんのリーク例はすべて熟練者による検査を経て、目録にまとめてあります。
そして喜ばしいことに、このスペースリークの動物園を公の場に解放することになりました。
これからいくつかの種類のスペースリークを紹介しますが、すべて別物ですので、訪問者のみなさんは混同なさらないようにお願いします。
(実際にそのスペースリークに遭遇したときの取り扱い方も違ってくるので、間違った方法を使ったら状況を悪化させてしまいかねません。)
メモリリーク
メモリを解放してOSに返すことができないことはメモリリークと呼ばれます。
これはレアモノです。というのも、ガベージコレクションによって絶滅寸前なのです。
このシリーズではこのようなメモリリークの例は出しませんが、厳密なことを言えばFFIの低レベルなメモリ確保関数をbracketなしで使った場合はHaskellでもこのようなメモリリークが起きることがあります。
強い参照によるリーク
原理的には使われる可能性があるが、実際にはもう使われないメモリをプログラムが参照することを「強い参照によるリーク」と言います。
純粋で遅延評価をするHaskellらしいプログラムでは、このようなリークはあまり多くありません。
純粋性はミュータブルな参照(適切なタイミングでクリアされなければメモリリークを起こします) の使用を防ぎ、このようなリークを避けます。
遅延評価はデータの一部しか必要でない時にデータ構造を無駄に生成しないようにして、やはりこのようなリークを防ぎます。
もちろん、ミュータブルな参照や正格評価を使うとこのようなエラーを引きおこすことがあります。
(ミュータブルな参照によるものは"強い参照"という名が示すとおり、弱い参照(System.Mem.Weak)を用いることで解決できることがあります。)
また、下記の「生きた変数によるリーク」は強い参照によるリークの一種で、クロージャに不慣れな人を驚かせることがあります。
サンクのリーク
たくさんの評価されていないサンクをプログラムがメモリ内に作り出し、大量のスペースを占有することを「サンクのリーク」と言います。
これはヒーププロファイルでたくさんのサンクを見つけた時や、ずらずらと連なったサンクの評価中にスタックオーバーフローを起こしたときに気づくことができます。
このようなリークは遅延評価でよく見られ、手続き型の世界では比較的まれです。適切に正格評価を導入することで解決できます。
生きた変数によるリーク
「生きた変数によるリーク」はクロージャ(サンクかラムダ式)が、プログラマがもう解放したと思っているメモリへの参照を持っている場合に起きます。
これはサンクや関数の中のメモリへの参照が、(データレコードのように)明示的ではなく、(生きている変数であるために)ユーザーに気づかれにくくなりがちであることによって起こります。
関数の結果が比較的小さい場合では、リークは正格性を導入することで解決できます。
しかし、直すためには全ての参照を確実に取り除かなくてはいけないため、これはサンクのリークほど直しやすくありません。
さらに、評価済みのメモリの大きなかたまりが存在しても、生きた変数によるリークが起きているとは限りません。
もしかすると、下に説明するように、ストリーミングに失敗しているのかもしれません。
ストリーミングリーク
ストリーミングリークは、ストリーミングの際に入力と出力の両方がとても少ない場合に起こります。
メモリ使用量もとても少なくなりそうですが、そうはならないのです。
それどころか、大量の入力が強制的に評価され、メモリにたくわえられてしまいます。
このようなリークは遅延評価と中間データ構造で起きますが、サンクのリークとは違って正格評価の導入は状況を悪化させかねません。
作業を複製し、データの依存を注意深く追跡することで直すことができます。
スタックオーバーフロー
プログラムが今処理していることの後にたくさんの処理待ちの作業を作ってしまうことをスタックオーバーフローと言います。
これはスタックスペースを使いきったときに顕在化します。
厳密に言えばこれはスペースリークではありませんが、サンクのリークやストリーミングリークを直す方法を間違えるとスタックオーバーフローになることがあるため、ここに掲載しました。
(サンクのリークとは別物ですが、同じように見えるときがあります。)
このリークは再帰によって起こります。
これは、再帰しているコードをイテレーションや、もっと良いデータ構造に書きなおして末尾再帰最適化されるようにすることで直せます。
最適化をオンにするだけでも良くなることがあります。
セレクタリーク
セレクタリークはサンクのリークの変種で、データの一部しか使わないのに、サンクが評価されていないためデータ全体が保持されることを言います。
これはランタイムシステムによるGHCのselector thunks最適化によってほとんど防がれますが、下記のように、最適化の失敗によってたまに引き起こされることもあります。
最適化によるリーク
最適化によるリークはこれまでのリークのいずれかがカモフラージュされているものです。
ソースコードを見るかぎりではリークはないように見えるのですが、コンパイラの最適化がリークを引き起しているのです。
これは本当に特定しにくいです。「動物園」で取り扱うには不向きですね!
(これを直そうとするとGHCのバグトラッカーに投稿しなくてはいけません)
スレッドリーク
あまりにたくさんのHaskellのスレッドが作られてしまうことをスレッドリークと言います。
これはヒーププロファイルに"TSO"が表れることによって特定することができます。TSOとはスレッドステートオブジェクトの略です。
これのデバッグは興味深いです。というのも、スレッドが死なない理由はたくさんありうるからです。
次の記事では、いくつか図を用いてこれらのリークの例を上げてみます。
興味を持った方は、練習として前回見たリークを分類してみるのもいいでしょう。
更新
サンクのリークと生きた変数によるリークを分離し、主に強い参照に関するいくつかの点をさらに明確にしました。
後の記事で、これらの概念上の重要な違いだと思われる点を詳しく説明するつもりです。