- CPython PR #116338 は、free-threaded ビルドで
PYTHON_GIL=0 または -X gil=0 により GIL を無効化できるようにする変更を python:main にマージした
- 実行時に GIL を再度有効化できる可能性を残すため、GIL 関連のデータ構造は通常どおり初期化し、無効化は起動時にフラグを設定して
take_gil() と drop_gil() が早期リターンするように処理する
- 初期確認では、
PYTHON_GIL=0 設定でスレッドを使わない一部のテストと小さなプログラムは正常に動作し、ごく基本的なスレッドプログラムは時々動作したが、テストスイート全体は test_asyncio で早々にクラッシュした
- レビュー過程で
PYTHON_GIL のテスト、ドキュメント化、-X gil オプション、sys.flags への反映が追加され、PYTHON_GIL=1 が GIL 有効化を強制するように設定処理も修正された
- 後続作業は、互換性のない拡張をロードする際に GIL を再度有効化する問題と、GIL をデフォルトで無効化する問題に分けられ、この変更は Python 3.13 の free-threaded ビルドにおける GIL 制御のインターフェースを追加する
マージされた変更
- CPython PR #116338 は
gh-116167: Allow disabling the GIL with PYTHON_GIL=0 or -X gil=0 の変更を扱う
colesbury が 2024 年 3 月 11 日に python:main へマージした
- 変更規模は 12 ファイル、163 行追加、1 行削除 と表示されている
- 対象機能は通常ビルドではなく、free-threaded ビルドで GIL を無効化する実行オプションである
GIL 無効化の方式
- free-threaded ビルドでは次の設定で GIL を無効化できる
- 実行時に GIL を再度有効化できるよう、すべての GIL 関連データ構造は通常どおり初期化される
- 実際の無効化は起動時にフラグを設定する方式である
- このフラグにより
take_gil() と drop_gil() は早期リターンする
- レビュー中には、
PYTHON_GIL=1 のとき enable_gil を正しく設定するコミットも追加された
テストと現在の制約
PYTHON_GIL=0 設定で一部のテストと小さなプログラムを確認した
- スレッドを使わないテストと小さなプログラムは正常に動作することが確認された
- ごく基本的なスレッドプログラムは時々動作した
- テストスイート全体はすぐにクラッシュし、場所は
test_asyncio と記録された
!buildbot nogil コマンドにより NoGIL 関連ビルダーテストが複数回予約された
x86-64 MacOS Intel ASAN NoGIL PR
x86-64 MacOS Intel NoGIL PR
ARM64 MacOS M1 Refleaks NoGIL PR
ARM64 MacOS M1 NoGIL PR
AMD64 Ubuntu NoGIL Refleaks PR
AMD64 Ubuntu NoGIL PR
AMD64 Windows Server 2022 NoGIL PR
レビュー中に追加された範囲
corona10 は Lib/test/test_cmd_line.py に環境変数テストを追加する価値があると提案した
- その後、次のコミットが追加された
Add test for PYTHON_GIL in test_cmd_line
Set enable_gil properly when PYTHON_GIL=1
Don't add 'enable_gil' to test_embed in normal builds
colesbury は、環境変数を追加する時点でドキュメント化するのがよいと考えた
- その根拠として、すでに
--disable-gil configure フラグは文書化されている点を挙げた
- ドキュメントには、free-threaded ビルドでのみ使用可能であること、
0 は GIL 無効化を強制し、1 は GIL 有効化を強制し、Python 3.13 の新機能であることを含めるべきだと整理した
- その後
Document PYTHON_GIL environment variable コミットが追加された
-X gil オプションの追加と最終マージ
- Discord での議論の後、環境変数とあわせて使える
-X オプションも追加することになった
- PR タイトルは
PYTHON_GIL=0 のみを扱う形から、PYTHON_GIL=0 or -X gil=0 まで含む形に変更された
- 追加コミットには次の内容が含まれる
Add -X gil option, add to sys.flags, modify test to cover env var… and option
Fix link to -X gil
Fix PYTHON_GIL versionchanged line
Clarify test_flags in normal builds
ericsnowcurrently、erlend-aasland、corona10、colesbury が変更を承認した
- マージコミットは
2731913 で、マージ後に vstinner はこの変更について「興味深く、非常に恐ろしい」と反応した
後続作業
- 後続の issue として 2 つの作業が分離された
- 現在の PR は GIL のデフォルト値変更ではなく、free-threaded ビルドでユーザーが環境変数または
-X オプションにより GIL の状態を制御できるようにする変更である
1件のコメント
Hacker News のコメント
no-GIL の取り組みに興味がある人向けに、追加リンクを残しておく: [0], [1]
[0] Multithreaded Python without the GIL
https://docs.google.com/document/d/18CXhDb1ygxg-YXNBJNzfzZsD...
[1] Github repo
https://github.com/colesbury/nogil
[0] https://peps.python.org/pep-0703/
[1] https://github.com/colesbury/nogil-3.12
標準の Python をどこまで速くできるのか期待している。この問題を緩和しようとするツールが増えすぎて、Python の価値提案も揺さぶられている
高速化ツールとしては Mojo、pytorch、triton、numba、taichi が思い浮かぶ。この問題を解こうとする試みがあまりに多く、前にどれかを使ってみようとしたときは選択肢が多すぎて圧倒された。結局 taichi を選んだが、かなり面白く使いやすかった一方で、適用範囲はやや限定的だった
Taichi は本当に過小評価されている。Metal を含むすべてのプラットフォームで動作し、例が多く、コードも書きやすい。何よりエコシステムと統合され、既存のエコシステムを置き換えない
https://github.com/taichi-dev
Taichi で何ができるかを示す優れたデモ動画: https://www.youtube.com/watch?v=oXRJoQGCYFg
https://www.youtube.com/watch?v=WNh4Q7-OSJs
https://www.taichi-lang.org/
https://peps.python.org/pep-0703/ で説明されている biased reference counting の方式が、なぜ単一スレッド親和性だけを持たせ、他のスレッドからアクセスするとアトミックなインクリメント/デクリメントを要求するのか気になる
他の実装、たとえば biased reference counting を実装した複数の Rust クレートでは、新しいスレッドへ移すときだけアトミックに増やし、そのスレッドは再び 0 に達するまで非アトミックなインクリメント/デクリメントを行い、最後にアトミックなデクリメントを行う方式を見たことがある。既存システムに付け足す形なので単一の PyObject があり、新しいスレッドローカルなオブジェクトを指すように置き換えられないからなのか気になる
Rust では所有権移転のための「move」が言語の一部だが、C や Python にはそれに対応する概念がないため、いつ所有権を移すべきか、どのスレッドが新しい所有者になるべきかを判断するのが難しい。ヒューリスティックは使える。たとえばオブジェクトを queue.SimpleQueue に入れるときに所有権を放棄または移転できるかもしれないが、その場合でもキューに入ったオブジェクトをどのスレッドが「get」するかを事前に知るのは難しい
性能上の利得も小さいと思う。多くのオブジェクトは単一スレッドからしかアクセスされず、一部のオブジェクトは複数スレッドからアクセスされるが、あるスレッドからだけ排他的にアクセスされた後、別のスレッドからだけ排他的にアクセスされるオブジェクトはまれだ
先に tranched bread のニュースを読んだのに、今度はこれまで? すごい時代だ
Unladen Swallow プロジェクト [1] が尻すぼみになったときは少し残念だった。Python が再び中核的な最適化の道に戻ってくるのを見るのはうれしい
[1] https://en.wikipedia.org/wiki/CPython#Unladen_Swallow
5歳児に説明するように教えてほしい
GIL が何なのかは概念的には分かる。けれど、この変更の影響は何なのか? 全体的な性能向上を期待しつつ、これからパッケージが壊れることになるのか?
高負荷の CPU 作業でなくても、この変化は有用になり得る。最近は多くのコードが Python のネイティブな asyncio 言語機能で書かれている。これは NodeJS のように async/await で実行を譲りながら単一スレッドで動作し、単一スレッドだけでも秒間数千リクエスト程度のかなり良いスループットを出せる
しかし大きな問題は、何らかの CPU 作業 を実行した瞬間に他のすべてのコルーチンをブロックしてしまい、さまざまな曖昧な問題が生じ、秒間リクエスト数が崩れることだ。たとえば、あるコルーチンでランダムな入出力タイムアウトが見えるが、実際の原因はまったく別のコルーチンがしばらく CPU を占有していたことかもしれない。なぜこうしたことが起きるのかを観測するのも非常に難しい。asyncio にはブロッキング作業をメインスレッド外へ逃がすのに役立つ
asyncio.to_thread()関数 [1] があるが、GIL のせいで CPU 中心の作業が他のコルーチンに干渉しないよう本当に分離することはできない[1] https://docs.python.org/3/library/asyncio-task.html#asyncio....
興味がある人向けに言うと、GIL は Global Interpreter Lock の略だ
ここでのより大きな全体像をうまく整理した資料はあるだろうか?
ついに各種ツールの ベンチマーク が楽しみだ