PythonのGarbageCollection

原文

Neil Schemenauer (翻訳:中村 成洋)

ポータブルなGarbageCollection

概要

循環参照はリスト,タプル,インスタンス,クラス,辞書,関数に伴って見つかります. インスタンスの __del__ メソッドは正常に取り扱われます. 新しいタイプをGCの対象に追加するのは簡単です. このGCが有効なPythonは,通常のPythonとバイナリ互換です.

世代別GCが動いています(今は三世代).このオーバヘッドをpybenchで測ったら,大体4%くらい占めていました. 実質的に,すべての拡張モジュールは,不変に(私は,標準的な配布において 新しいものとcPickleを修正しなければなりませんでした)ならなければなりま せん.gcと呼ばれている新しいモジュールは,コレクターを調整して,デバッ ギングオプションをセットすることに利用できます.

GCはあらゆる環境で動作するようなポータブルなものである必要があります. このPythonのパッチバージョンでは全ての回帰テストにパスし,Grail,Idle,Sketch が問題なしに動作します.

このポータブルなGarbageCollectionはPython2.0から取り込まれ,現在,有効になっています.これはとても喜ばしい事です.

なんでこれが必要だったの?

現在のバージョンのPythonは参照カウントを使って,メモリ確保の経過を保持しています.

Pytnon内の全てのオブジェクトは参照カウントを持っています.これはいくつのオブジェクトが自分を指しているのかを表しています. この参照カウントが0になったとき,オブジェクトを解放します.この手法は大抵のプログラムにはうまく動作します.

しかし,この参照カウントには循環参照と呼ばれる1つの基本的な欠陥があるのです. その簡単な例としては,自分自身の参照先を自分にしてしまうというのがあります.例えば

>>> l = []
>>> l.append(l)
>>> del l

といった具合です.

この例では一つのリストを生成しました.しかし,このリストはプログラムから二度と使う事ができません. つまり,このリストはゴミであると考えられます.しかし,現在のバージョンのPythonではこのリストを解放する事はできません.

循環参照が作られるのは通常,行儀のいいプログラミングとはいえません.それは回避する事が可能です. しかし,時々,循環参照を避けるのが非常に難しい場合があります.また,循環参照が起こっている事を気がつかない場合もあります. サーバのような長い継続的なプログラムの場合は特に面倒な事になります. 人々は,参照カウントが使用できないメモリ領域を解放出来ずに,OutOfMemoryになる事を望みません. また,大きいプログラムの場合は,循環参照がどこでどのように作られているか見つける事は難しいでしょう.

"伝統的な" GarbageCollectionってどんなものなの?

伝統的なGarbageCollectionは(ここで述べるのは mark sweep や stop and copyの事)次の様な動作をします.

残念ながら,このアプローチは現在のPythonには適用できません. なぜなら,拡張モジュールが動作している為,Pythonはルートセットを完全に取り決める事は決して出来ないからです. ルートセットが正確に決定できない場合は,オブジェクトを解放する際に「どこか」から参照されている危険性があります. たとえ,拡張モジュールが現在と違う設計をされたとしても,オブジェクトはCの関数スタック上にあり,ポータブルにルートを見つける方法はないでしょう. それに,参照カウントはメモリの局所的な場面において,いくつかの素晴らしい利益をもたらします.また,参照カウントではfinalizeがPythonプログラマの想像通りの場所で起こるでしょう. もっとも重要な事は,我々が参照カウントを効率的に使う方法を見つけられたかどうかという事です.循環参照の問題がありましたよね.

じゃあどんなアプローチをしたの?

概念的に,このアプローチは,いわゆる"伝統的な"GCの正反対の事をやります. それは,すべての辿れるオブジェクトを見つけるのではなく,すべてのもう辿る事のできなくなってしまったオブジェクトを見つけるという手法です. この方法はとても安全だと考えています.なぜなら,もしこのアルゴリズムが失敗しても,GCがない状態より悪くなる事はないからです.(無駄になる時間と空間を除いて)

さて,我々は参照カウントをいまだに使いつづけているため,循環参照を見つけねばなりません. 「参照カウント」は他のタイプのゴミを取り扱います. 最初に,我々は循環参照がコンテナオブジェクトによってのみ作られるという事を注意しておかねばいけません. これらのオブジェクトは他のオブジェクトに対する参照を保持する事ができます. コンテナオブジェクトの例として,タプル,リスト,および辞書が挙げられます. 数値と文字列はコンテナオブジェクトではありません. これらの事柄から,コンテナオブジェクトで無いものは,GCを行う際に無視できると実感できます. 整数と文字列は小さく,速くなければなりませんので,これは役に立つ最適化です.

我々は全てのオブジェクトの経過を追う事を考えています. これを実現するにはいくつかの方法があります. これらの中で最高の選択は,オブジェクトの構造体の中にフィールドを持たせ て二重リンクのリンクリストにしようというものです. この方法は不要なメモリアロケーションをせず, セットからオブジェクトを素早く挿入,削除する事が可能です. コンテナが作られた際には,このセットに挿入されます,そして削除されるときには取り除かれます. 今,我々は全てのオブジェクトに対してアクセスする手段を得ました,さて,どのように循環参照を見つけるのでしょうか. 最初に我々はコンテナオブジェクトに別のフィールドを追加しました. 我々はそのフィールドを gc_refs と読んでいます. 循環参照を見つける手順は次の様なものです.

  1. 全てのコンテナオブジェクトの,gc_refsにオブジェクトと同じ参照カウントを設定します.
  2. 全てのコンテナオブジェクトに対して,そのオブジェクトがどのコンテナオブジェクトを参照するかを探して,参照先のコンテナの gc_refs をデクリメントします.
  3. 現在,gc_refsフィールドで1を超えている,すべてのコンテナオブジェクトは,コンテナオブジェクトのセットの外からリファレンスをつけられています.我々はこれを解放する事はできませんので,違うセットに移動します.
  4. また,移動するコンテナオブジェクトから参照されている,少しのオブジェクトも同じく解放できません.なので,そのオブジェクトも違うセットに移動します.とまた移動するオブジェクトがでましたね.このオブジェクトに関しても,オブジェクトから参照できる全てのオブジェクトも一緒の処理を行います.
  5. 移動元のセットに残されたオブジェクトは,セット内のオブジェクトのみから参照されているものだけになります(つまり,Pythonプログラムからアクセスできないゴミです)我々はこれらを解放して回る事ができます.

ファイナライザのトラブル

我々のこの壮大な計画に関する1つの問題があります.ファイナライザの使用です. Pythonにおいて,これはインスタンスの __del__ というメソッドを使用してファイナライザを設定します. 参照カウントでは,ファイナライザはとても良く動きます. なぜなら,オブジェクトの参照カウントが0になり,オブジェクトが解放される前に,ファイナライザは呼び出されるからです. これはプログラマにとって直感的で理解しやすいでしょう.

しかし,GCで,特に循環参照に直面した場合,ファイナライザは難解な問題になります. もし,二つのファイナライザを持つオブジェクトが循環参照であった場合,どうすればいいでしょう?どちらのファイナライザを先に呼べばいいでしょうか? 第1のオブジェクトのファイナライザを呼んだ後,第2のオブジェクトのファイナライザで,第1のオブジェクトを使う可能性があり,第1のオブジェクトを解放する事はできません.

この問題に対するいい解決方法はないので,循環しているオブジェクトにファイナライザがある場合,そのオブジェクトは解放しません. その代わり,それらのオブジェクトは「解放できないゴミ」としてグローバルリストに追加します. これが起こらないように,プログラムで修正することが可能でなければなりません. 最後の手段として,プログラムからグローバルリストにアクセスして循環を解放する事ができます.これはアプリケーション側で行うのが理にかなっています.

どういうコストがあるの?

一部の人々が言うように,お得なサービスランチなんてものはありません. しかし,このGCの形式はかなりお得な方です. 最大のコストはコンテナオブジェクト毎に必要な3ワード分の余分なフィールドです.コンテナをセットに維持するコストもあります. このGCによる速度低下は,pybenchによる測定だと4%程度です.

GCは現在オブジェクトの3世代の経過を追います.GCのパラメータを調整する事で,GCに費やされる時間を要望通りにすることができます. いくつかのアプリケーションでは自動でGCが走る事を抑制したり,明示的にGCを呼んだりする方が効率がいいかもしれません. デフォルトのGCパラメータでpybenchを走らせた際に,GCに掛かった時間はあまり重要ではありません. 多くのオブジェクトを割り当てるアプリケーションは,明らかにGCの費やされる時間よりも,多くの時間が別の要因にかかります.

現在のパッチは,configure に追加したオプションで有効になります. コレクタ同梱のPythonは,標準的なPythonとバイナリ互換です. オプションを使用不可にすれば,Pythonインタプリタには影響がでません.

その他

もしあなたがメモリリークの調査をしたいならば,Tim Peter'sのCyclopsってモジュールが非常に使いやすいです. それはCyclopsからダウンロード可能です.

翻訳者コメント

翻訳は,中村 成洋<authornari@gmail.com> が行ないました.

この翻訳の公開にあたっては原著者の許可を得ています.再配布や別サイトで の公開については原著者の許可を得ていませんので,各人の判断でお願いしま す.もちろんリンクは自由です.

もしも,誤植や誤訳などがありましたらお気軽に authornari@gmail.comまでお知らせ ください.

Copyright (C) 2009 中村成洋

http://www.narihiro.info/