3 ポイント 投稿者 GN⁺ 2025-03-11 | 1件のコメント | WhatsAppで共有
  • CPythonプロジェクトは最近、バイトコードインタプリタに新しい実装戦略を導入した。初期結果では、さまざまなプラットフォームで平均10〜15%の性能向上が示された
  • しかし、この性能向上は主にLLVM 19のリグレッションを回避した結果だった。より適切な基準(例: GCC、clang-18、特定のチューニングフラグ付きLLVM 19)と比較すると、性能向上は1〜5%に縮小した

性能結果

  • 複数のコンパイラと構成オプションを使って、CPythonインタプリタの複数のビルドをベンチマークした。IntelサーバーとApple M1 Macbook Airでテストした。
  • すべてのビルドでLTOとPGOを使用した。clang18を基準として、pypeformance/pyperf compare_toで報告された平均を使用した。
  • コンパイラ性能比較
    • Apple M1 Macbook Air:
      • clang18: 基準
      • clang19: 1.12倍遅い
      • clang19.taildup: 1.02倍遅い
      • clang19.tc: 1.00倍遅い
      • gcc: N/A
  • 末尾呼び出しインタプリタは依然としてclang-18と比べて高速化を示したが、clang-19への移行に伴う速度低下の方がより顕著だった。

LLVMリグレッション

簡単な背景

  • 従来のバイトコードインタプリタは、whileループ内のswitch文で構成される。ほとんどのコンパイラはswitchをジャンプテーブルにコンパイルする。
  • 現代のCコンパイラは、ラベルのアドレスを取得してそれを「computed goto」として使うパターンをサポートしている。CPythonは末尾呼び出しの作業以前までこのパターンを使っていた。

LLVM 19のリグレッション

  • LLVM 19はtail-duplicationパスに制限を設け、IRサイズが特定の閾値を超えた場合に複製を打ち切るようにした。これによりCPythonではすべてのディスパッチジャンプが統合され、computed gotoベース実装の目的が完全に失われた。

追加の異常

  • 末尾呼び出し複製ロジックの変更がリグレッションを引き起こしたことは確信しているが、リグレッションの大きさを完全には説明できない。
  • 現代のプロセッサでは、2〜4%の高速化の方がより一般的である。

computed gotoは必要か?

  • clang19.nocgベンチマークはclang19より速いと主張している。これはコンパイラがswitchベースのインタプリタを使って同じ最適化を行えることを示している。

修正

  • LLVMプルリクエスト114990がこのリグレッションを修正した。この修正によって期待された性能が回復した。

振り返り

ベンチマークについて

  • システムの最適化時には、ベンチマークとベンチマーク手法を構成し、提案された変更を評価する。
  • ベンチマークは、特定のデータポイントを一般化するために、より多くの仮定と信念を必要とする。

ベースライン

  • 新しい解決策や手法を提案する際には、「現在知られている最善のアプローチ」と比較するのが一般的である。

ソフトウェアエンジニアリングについて

  • ソフトウェアシステムは複雑で相互接続されており、急速に変化している。
  • 最適化コンパイラは、プログラマの意図を尊重しつつコードを最適化しなければならないという緊張関係の中にある。

最適化コンパイラ

  • musttail属性は、最適化に関わる新しい種類のコンパイラ機能を表している。これは性能に敏感なコードを書くための、より強力なスタイルを提供しうる。

nixについてもうひとつ

  • nixはこのプロジェクトで非常に有用だった。複数バージョンのPythonインタプリタの管理とビルドに大いに役立った。

1件のコメント

 
GN⁺ 2025-03-11
Hacker Newsのコメント
  • こんにちは。私はCPythonにtail-callingインタプリタを導入したPRの作成者です

    • まず、この問題の根本原因を突き止めるためにほぼ1か月を費やしたNelsonに感謝を伝えたいです
    • また、このような大きなミスを犯してしまい、とても恥ずかしく、申し訳なく思っています
    • 私たちが使っていたコンパイラにこのようなバグがあるとは、CPythonチームも予想していませんでした
    • おわびのブログ記事をここに掲載しました: リンク
  • ベンチマークは本当にうまくやるのが難しい作業です

    • 最近、アルゴリズムを約15%高速化する方法を見つけました
    • しかしテスト中、より高速なバージョンの関数を呼び出していないにもかかわらず、元のコードが15%速くなりました
    • これはコードとメモリ配置の問題で、CPUキャッシュとの整列がより良くなったためでした
    • Casey Muratoriはこの種のテーマについて興味深いシリーズを進めています
  • 著者がこの問題の真相を掘り下げたことに賛辞を送ります

    • Python 3.14のtail-callインタプリタは、依然として良い改善です
    • この出来事は、ベンチマーキングの厳密さと、さまざまな環境でテストする重要性を教えてくれました
    • また今では、誰にとっても利益になり得るコンパイラのバグを発見したことになります
    • どれほど多くの「X%高速化」という結果が、実際にはベンチマークのアーティファクトや未知の回帰によるものなのか気になります
  • Cが「機械に近い」言語ではないことを示す良い例です

    • clang-19はcomputed gotoインタプリタを「正しく」コンパイルしますが、最適化の意図とはまったく異なる出力を生成します
    • 他のコンパイラのバージョンも、「素朴な」switch()ベースのインタプリタに最適化を適用します
  • コンパイラがループを構成する方法を調整することで、tail-callインタプリタは発表されたほど効果的ではなくなります

    • CPUアーキテクチャとバージョンが非常に重要です
    • Cの抽象機械は、意図を適切に表現するには十分に低水準ではありません
    • 特に偏執的なインタプリタ実装では、直接アセンブリを書くところに回帰します
    • luajitはマクロシステムを実装し、効率的なアセンブリループ実装をアーキテクチャ間で移植可能にしています
  • Pythonビルドの性能を評価するのは非常に難しいです

    • 最近、astralチームがconda-forgeビルドは他の大半のビルドより高速だと示しました
    • tail-callインタプリタが他のビルド最適化と組み合わせてどのように動作するのか気になります
  • 関連する議論:

  • 素晴らしい記事です

    • 参照されている記事の1つでは、3.14.0a5は3.13より1.12倍速いと述べられています
    • ベンチマークを、別のプロセスで過負荷になった状態で実行したのかどうかが気になります
    • ベンチマークは、外部変数を排除するために厳密に管理された環境で実施されるべきです
  • 最近、Python 3.9から3.13までのベンチマークを行いました

    • 3.11までは性能が改善しましたが、3.12と3.13は3.11より約10%遅くなっていました
    • 自分のベンチマークだけでは十分ではないと思っていましたが、中核サービスにデプロイした際にも同じ変化を観測しました
  • この種の最適化がtail-call最適化とどう関係しているのか気になります

    • インタプリタのジャンプテーブル実装は、スタックフレームの生成に影響しないはずです