Pythonは、すべての変数などが参照しているオブジェクトを正確に把握し、それぞれのオブジェクトが参照されている数を厳密に記録しています。この記録されている参照の数を、参照カウント と言います。参照カウントが 0 になると、そのオブジェクトはすでに利用されていないことが明らかなので、Pythonはそのオブジェクトを開放します。
参照カウントを更新する際にはデータ競合を避けるためにロックが必要となりますが、GILがなければオブジェクトが参照されるたびにロックの取得と開放が必要となってしまいます。これによりパフォーマンスが大きく低下するため、参照カウントはGILの除去が難しい大きな要因とされてきました。
PEP-703では、参照カウントの更新に伴うパフォーマンス低下を避けるため、以下の手法が提案されています。
オブジェクトの永続化¶
現在のCPythonでは、None
やTrue
、False
などの定数や、整数の0
や 1
などのオブジェクトについても参照カウントで管理されていますが、これらの値は起動時に生成され、決して開放されることはありません。そこで、これらのオブジェクトに関しては参照カウントの更新を行わないことにして、ロックが不要となるように変更します。組み込みの型オブジェクト(int
, list
, dict
など)も永続化されます。
バイアス付き参照カウント¶
オブジェクトの参照は、ほとんどの場合そのオブジェクトを作成したスレッドで行われます。たとえば、
a = 1000
b = 2000
c = a+b
print(c)
という処理は、整数の 1000
、2000
, 3000
オブジェクトを参照しますが、これらのオブジェクトは他のスレッドから参照されていません。
そこで、バイアス付き参照カウント では、オブジェクトを作成したスレッドからの参照と、他のスレッドからの参照を別々の領域でカウントします。こうすれば、同じスレッドからの参照はロックをせずに直接参照カウントを更新できます。したがって、ロックを利用してデータ競合を回避するのは、他のスレッドからの参照のみで済むようになります。
遅延参照カウント¶
ほとんどのオブジェクトは作成元のスレッドから参照されますが、他のスレッドから参照されることも多いオブジェクトも存在します。モジュールや関数などのオブジェクトは、作成されたあと、各スレッドでインタープリタから頻繁に参照されることになります。
しかし、関数オブジェクトなどは、一般のリストや辞書オブジェクトと違ってそれほど頻繁に作成したり削除したりするものではありません。そこで、インタープリタが関数の実行時などに参照する際には、参照カウントはスレッドの特別な領域に記録しておきます。これにより、オブジェクトの呼び出しはロックを獲得せずに高速に実行できます。記録した参照カウントはのちにガベージコレクター実行時に集計して、必要があれば開放します。
型オブジェクトの参照カウント¶
型オブジェクトもまた作成元スレッド以外から頻繁に参照されるオブジェクトですが、こちらはインタプリタからの直接参照以外に型のインスタンスからも参照されるので、遅延参照カウントだけでは効率化できません。そこで、インスタンス作成・開放時などの型オブジェクトへの参照は、スレッドごとの専用の領域に参照カウントを記録します。この参照カウントは、スレッド終了時に集計されます。