8月31日、Isabella Muerte氏が「500 Python Interpreters」と題したブログ記事を公開し、話題となっている。この記事では、Python 3.13の最終リリースを前にして、PythonのGIL(Global Interpreter Lock)のオプション化と、その背景にある複数の取り組みについて詳しく紹介されている。
PythonのGILとは
Pythonの GIL(Global Interpreter Lock) は、Pythonのマルチスレッド処理において長年にわたり重要な役割を果たしてきた。このロックは、同時に複数のスレッドがPythonオブジェクトにアクセスすることを防ぐために設けられたものである。
しかし、その一方で、GILはPythonのマルチスレッドパフォーマンスを制約する要因でもあった。この問題を解決するために、Python 3.13ではGILをオプション化するPEP 703が提案されており、Python 3.12では各インタープリタごとに独立したGILを持つPEP 684が導入された。
PEP 703: Python 3.13でのGILのオプション化
PEP 703は、Python 3.13でGILをオプション化するための提案である。これにより、開発者はGILを無効にすることができ、マルチスレッドアプリケーションのパフォーマンスが大幅に向上することが期待されている。GILのオプション化は、特に並列処理が必要なタスクにおいて重要なステップである。
GILのオプション化の背景には、Pythonが他の言語に比べてマルチスレッド処理で劣っているという認識がある。これにより、Pythonを選択する開発者が減少する可能性があり、Pythonコミュニティはこの問題に対処するためにGILをオプション化することを決定した。GILを取り除くことで、スレッド間の競合が減少し、特にCPUバウンドなタスクのパフォーマンスが向上する。
PEP 684: Python 3.12でのPer-Interpreter GIL
PEP 684は、Python 3.12で導入された各インタープリタごとに独立したGILを持つ仕組みを提案している。この提案の目的は、複数のインタープリタが同時に動作する際に、それぞれが独自のGILを持つことで、スレッドの競合を減少させることである。
これにより、マルチインタープリタ環境でのパフォーマンスが向上し、特に並列処理が必要なアプリケーションにおいて効果を発揮する。具体的には、Webサーバーや大規模なデータ処理システムなど、多数のスレッドが同時に動作する環境で、各インタープリタが独立して動作できるようになる。
PEP 703とPEP 684の連携
PEP 703とPEP 684は、それぞれ独立した提案ではあるが、連携してPythonの並列処理を大幅に改善することができる。PEP 703がGILのオプション化を実現することで、開発者はGILを無効にした環境で並列処理を行うことが可能になる。一方で、PEP 684により、各インタープリタが独自のGILを持つことで、並列処理がさらに効率的になる。
これにより、GILをオプション化した環境でも、複数のインタープリタが独立して動作することが可能となり、大規模な並列処理タスクにおいて高いパフォーマンスを発揮することができる。
500個のインタープリタを立ち上げた実験
記事の著者であるIsabella Muerteは、Pythonのスレッドサポートを検証するために、500個のインタープリタを立ち上げ、それぞれでPythonコードを実行する実験を行った。この実験では、各インタープリタが独立して動作することを目的としているが、実際にはいくつかの問題が発生した。
以下は、実際に使用されたコードサンプルである。
// Pythonのインタープリタを同時に立ち上げ、並列処理をテストするサンプルコード
// 最大インタープリタ数を定義
static constexpr auto MAXIMUM_STATES = 463;
// jthreadを使用してスレッドを管理
std::vector<std::jthread> tasks { };
tasks.reserve(MAXIMUM_STATES); // 事前にスレッド数分のメモリを確保
for (auto count = 0zu; count < tasks.capacity(); count++) {
tasks.emplace_back([config, count] { // 各スレッドでインタープリタを作成
if (auto status = Py_NewInterpreterFromConfig(&state, &config); PyStatus_IsError(status)) {
// インタープリタの初期化に失敗した場合のエラーメッセージ
std::println("Failed to initialize thread state {}. Received error {}", count, status.err_msg);
return;
}
// スレッド番号を表示するPythonコードを生成
auto text = std::format(R"(print("Hello, world! From Thread {}"))", count);
// グローバル名前空間を初期化
auto globals = PyDict_New();
// Pythonコードをコンパイル
auto code = Py_CompileString(text.data(), __FILE__, Py_eval_input);
// コンパイルしたコードを評価し実行
auto result = PyEval_EvalCode(code, globals, globals);
// 使用したオブジェクトを解放
Py_DecRef(result);
Py_DecRef(code);
Py_DecRef(globals);
// インタープリタを終了
Py_EndInterpreter(state);
// スレッドの状態をリセット
state = nullptr;
});
}
// これで、すべてのスレッドが生成され、それぞれがPythonコードを実行する
このコードでは、463個のインタープリタを並列で立ち上げ、それぞれが「Hello, world! From Thread X」というメッセージを出力するように設計されている。コードの各ステップには、スレッドの初期化、Pythonコードのコンパイルと実行、リソースの解放、インタープリタの終了が含まれており、それぞれが個別のスレッド内で実行される。
著者はこの実験中にメモリ破損や同期の問題が発生し、デバッグモードでは特に顕著であった。これらの問題は、Pythonのスレッドサポートにおける限界を示している。
GILオプション化とPer-Interpreter GILの課題と展望
GILのオプション化とPer-Interpreter GILの導入には、多くの利点がある一方で、いくつかの課題も存在する。まず、GILを無効にすることで、スレッド間の競合が増加し、スレッドセーフなコードを書くことが求められる。また、Per-Interpreter GILでは、各インタープリタ間のリソース共有が複雑になる可能性がある。
さらに、実際のパフォーマンス改善には限界があり、著者はPythonのスレッドサポートに対して厳しい見解を示している。著者は、特に高パフォーマンスが求められる場面では、V8や.NET Core、WASMなどの他のランタイムやVMがより適していると指摘している。
結論
GILのオプション化とPer-Interpreter GILの導入は、Pythonにとって大きな進展であり、並列処理のパフォーマンス向上に寄与するものである。しかし、実際の運用においては、他のランタイムやVMの方が優れている場合もあり、Pythonが今後どのように進化していくかは、さらなる検証と改良が必要である。
詳細は[500 Python Interpreters]を参照していただきたい。