原著者: | Unladen Swallowプロジェクト |
---|---|
原文: | http://code.google.com/p/unladen-swallow/wiki/ProjectPlan |
原文更新: | April 23, 2009 |
~Python最適化計画~
注意: 引用している論文はすべて、 “RelevantPapers”のページからリンクを張っている。中国語版もある
目次
このプロジェクトではPythonを速くしたいと思っている。また、大きくて安定しているアプリケーションをこのプロジェクト”Unladen Swallow”に切り替えて使用してもらえるようにするのもゴールの一つである。
パフォーマンスと互換性という、二つのゴールを同時に達成するために、我々はスクラッチから作るのではなく、CPythonを改修していくという方法を取ることを決めた。具体的には、CPython 2.6.1に対して作業を始めることにした。Python 2.6が、2.4/2.5(ほとんどのアプリケーションが使用している)と、3.x(最終的に到達する未来)の、間のちょうどいい位置にあるからである。CPythonのリリースコードから開始したおかげで、我々は組み込みの関数やオブジェクト、標準ライブラリモジュールなどの重要な部品を再実装しなくて済んでいる。また、頻繁に用いられているCPythonのC言語による拡張のAPIもそのまま使用することができるのである。2.XのCPythonからスタートしているため、既存のアプリケーションからは簡単に移行することができる。もし、3.Xから始めていたら、大きなアプリケーションのメンテナンスをしている人たちにアプリケーションの移植からお願いする必要があり、私達が対象としているユーザにうまくスタートしてもらうのを難しいと思っていただろう。
多くの時間をPythonコードの実行速度の方にフォーカスして行う仕事がメインとなる。Pythonのランタイムライブラリに使用する時間は少なくなるだろう。我々が長期間かけて提案したいのは、LLVM上に構築された、Python用のJIT機能付きのカスタムの仮想マシンを補完することである。それ以外のPythonランタイムに関してはほとんどCPythonのままになる予定である。我々が今までに行ってきた調査によると、Pythonのアプリケーションはメインのevalループの中で多くの時間を消費しているということが分かっている。特に、opコードのディスパッチといった、VMの構成部品に比較的小さい変更を加えるだけで、Pythonアプリケーションのパフォーマンスには著しい効果がある。我々は、LLVMのJITエンジンを通じて、Pythonをマシンコードにコンパイルすと、極めて大きなパフォーマンスの向上が得られると信じている。
いくつか明確になっているメリット:
マシンコードを生成するインフラを、Pythonをコンパイルする機能に加えると、現在のバイトコード表現で行えることよりももっと多くの効果的な実装が行えるようになる。例えば以下のようなコードがあったとすると:
for i in range(3):
foo(i)
現在は以下のように変換される
$x = range(3)
while True:
try:
$y = $x.next()
except StopIteration:
break
i = $y
foo(i)
もしrange()というのが組み込み関数のrange()のことであるというのを知ることができれば、我々は以下のように変換することもできるだろう。
for (i = 0; i < 3; i++)
foo(i)
C言語では、数値計算のためのアンボックス型を使用することもできる。また、以下のようにループを展開することもできるだろう。
foo(0)
foo(1)
foo(2)
我々は、Unladen Swallowの内部構造を、マルチコアが使用できることになることを想定した構造にしようと考えている。サーバにおいては、コア数は増える一方であり、より多くのタスクを並列でこなすために、性能を引き出せるようにしたいと考えている。例えば、他のコアで仕事をしつつ、より時間がかかるが性能の良い最適化を平行して行う並列コードオプティマイザを入れたいと考えている。また、並列ガーベジコレクションも導入して、負荷のかかっていない、あまっているコアを利用できるようにしたいと考えている。今日では、ほとんどのサーバの製品は、4個から32個のコアを載せて出荷されているので、私達はこの方向性での最適化が有利であると考えている。しかし、高度に並列化されたアプリケーションの需要については敏感に察知していかねば、と考えている。盲目的にコアを消費すればいいとは考えていない。
なお、我々が取り組もうとしている領域の多くの部分では、他の動的言語の実装がいくつも計画されていたり、開発されていたりする。例えば、MacRuby, JRuby, , Parrotや、他のPython実装である、Jython, PyPy, IronPythonなどである。我々はデバッグ情報に関するアイディアや、正規表現のパフォーマンスに関するアイディア、動的言語で一般的に使用されているパフォーマンスのためのアイディアというのを知るために、これらの他の実装を参考にさせてもらっている。これらは、すでに先人により踏み固められてきた部分であるため、一から車輪の再発明をすることは、できれば避けたいと考えている。
Unladen Swallowは三ヶ月ごとにリリースを行う。また、それとは別に必要に応じてその間にバグフィックスのためのリリースを行う。
Q1では、既存のCPythonの実装に対して、比較的小さな改修だけを行う。Q1で狙っているのは、標準の状態から25%から35%の高速化である。Q1の目標はできるだけ早く、既存のアプリケーションに対して、実際にパフォーマンスアップの恩恵を受けてもらうという、保守的なものである。プロジェクトの完了まで待ってもらうのは我々の望むところではない。
目標を達せするためのアイディア:
2009Q1リリースはrelease-2009Q1-maintというブランチで管理する。リリースとCPython 2.6.1とを比較して我々のパフォーマンスを見て欲しい。
Q2でフォーカスするのは、LLVMの作法に従って実装した、機能的に同等のPython VMに置き換えることである。我々はいくつかのパフォーマンス向上をanticipateしているが、2009Q2リリースではそこには軸足を置くことはしない。Q2ではあくまでも、LLVM上で動作する実装を手に入れるということに注力する。subsecuent半期後には、これによって多くの速度改善が得られる見込みである。
目標:
Q3以降(onward)の計画は単純に論文に従ってイテレーションをこなしていくだけである。我々は独自の作業は行わないつもりであるが、過去30年にわたる研究成果を取り込むつもりである。 完全なリストではないが、RelevantPapersのページにはその一部が含まれる。これらの論文の研究成果を引用して実装していくことを考えている。
我々は正規表現エンジンや、他のボトルネックが見つかった拡張モジュールのパフォーマンス改善についても計画をしている。しかし、正規表現はすでに、我々が改良を行っていくターゲットとしてふさわしいことは既に分かっているため、まず最初に最適化を行っていく。
それに加えて、我々が取り組もうと思っているは、GILの削除と、Pythonのマルチスレッドの状況の改善である。これは、IBMのRecycler(Bacon etal, 2001)のような、より”sophisticated “なガベージコレクションのシステムを実装することで可能になると信じている。
われわれの長期の目標は、パフォーマンス上重要なクラスと関数をCのバックエンドからPythonに持ってくることができるぐらい、Pythonを高速にすることである。
2009第三四半期のパフォーマンスの目標は第二四半期の終わりに決定する。
すぐにできる改良を行って、何人かの人に我々のPythonが適用できると確信してもらったら、LLVMへの切り替え作業に入る予定である。おそらく、最初はブランチを切って作業を開始するだろう。これによって数多くの作業が行えるようになる。
- クロージャー: クロージャーというのは、宣言された環境をパラメータとして設定された関数である。
- 例外のハンドリング – c版のevalの例外ハンドリング機構を変換する作業は順調とは言い難い状況である。CPythonは、ジャンプ先のバイトコードのインデックスを保存する。LLVMは命令のアドレスを保存することはできないため、例外ハンドラとして使用される基本ブロックと、適切なブロックにジャンプするswitch文を生成するようにしている。
- 関数呼び出し規則 – すべての引数を含むPyFrameObjectを渡している。生成コストは我々が期待しているものよりも高価である。PyFrameObjectはスタックのように動作し、関数から戻る時にローカル変数を戻すのに使用されるが、LLVMが最適化するのを妨げてしまうのである。
- ジェネレータ: 我々は、例外ハンドラで行ったのと同じように、yieldステートメントの数を数える予定である。そして、関数の中に、switch文を作成し、一度抜けたyield文から復帰できるポイントをブロックとして表現することを考えている。yieldの呼び出しの前後では、すべてのレジスタとローカル変数を保存する必要がある。幸いなことに、現在のCPythonのバイトコードからの変換作業の中では、スタックとローカル変数はすべて、return時にFrameObjectに保存するようにしているため、yieldからの復帰を実現するのも簡単にできる見込みである。しかし、フレーム内にローカル変数を保存するのをやめてしまうと、これを実現するのが難しくなってしまう。本来ならば、疑似レジスタの保存・復帰機能があればよかったが、残念ながら、現在のLLVMにはそのような機能は実装されていない。
7. 追加の最適化の追加の開始 Talin氏は、Pythonの関数呼び出しが間接的であるため、事前定義の最適化に関する技術を前もって適用するのは困難であると指摘している。そのため、直接呼出しができるように、型推論に取り組みたいと考えている。
8番目以降の項目は、2009の第二四半期の終わりに向けて、平行で作業を行っていく。
正規表現は伝統的なパフォーマンス上のホットスポットではないが、ほとんどの正規表現のユーザは、もっと早くなることを期待しているということが分かっている。また、結果として予想外のパフォーマンス劣化が発生することもある(訳不安)。このため、CPythonの正規表現エンジンの速度アップに対しても、リソースを投入するつもりである。
CPythonの現在の正規表現エンジンは、スタックベースのバイトコードインタプリタである。CPythonの正規表現エンジンには、命令コードのディスパッチのパフォーマンスを向上させるような、現代的な技術(Bell 1973; Ertl & Gregg, 2003; Berndl et al, 2005)の要素は取り込まれていない。その他の面では、伝統的で率直な実装のバーチャルマシンとなっている。我々は、Pythonそのものパフォーマンスの速度アップに使用される多くの技術は、同じように正規表現のパフォーマンス向上に対しても適用できると信じている。方法としては、まずはJITコンパイルされた正規表現をマシンコードに落とすことにより、命令コードのディスパッチの性能を向上させるところから始めるだろう。
JavaScriptのコミュニティが最近行っていることを見れば、我々が信じていたものは間違っていないということが分かる。GoogleのV8エンジンにはIrregexpというものが組み込まれている。これはJIT正規表現コンパイラである。また、SquirrelFish Extremeには同様の原理を基にした、新規の正規表現エンジンが含まれている。JITのコンパイル時間と、実行時間のトレードをするものである。これらは両方とも、さまざまなJavaScriptのベンチマークの正規表現の項目において、驚くほどの改善を見せている。我々が行いたいのは、これらの成果を、CPython上でも実現することである。
我々は、これとは別にシンプルな正規表現についてはRuss Coxが主張している、Thompson NFAを使用しようと考えている。これは、パターンによって最速の実装を選択できるような、複数エンジンの正規表現システムを作成するということである。V8チームも、Irregexpのワーキングではこのようなハイブリッドシステムを考えていたが、採用はされなかったという話を聞いた。以下のように述べている。
現在我々が取り掛かろうとしている問題は後方参照と、後方追跡の文脈で使用される|や*といった基本演算子である。正しい振る舞いにするためには、通常の正規表現のように、後方追跡が必要となる。Webで使用されている正規表現のデータをもとに、我々が考えている最適化が十分であるかどうか検証してみたが、この方法をとったサブセットによる利益はあまりにも小さいということがわかった。
CPythonの正規表現エンジンに対して仕事に取り掛かる前に解決しなければならない問題のひとつは、Pythonには正規表現のベンチマーク集が存在しないというという問題である。我々はV8ベンチマークのregexp.jsコンポーネントを再利用できないか、ということを考えているが、まず最初に、これらのベンチーマークがPythonのプログラムで書かれる正規表現の種類を網羅しているかどうかの検証をしなければならない。Pythonプログラムの中で使用されている正規表現が、JavaScriptやRuby, Perlなどで使用されるものと大きく違うと信じる理由は特にないが、確認が必要なことには変わりはない。
数多くのPythonのヘビーユーザと話をすると、Pythonの起動時間の改善に興味を持つ人は多い。これは、サーバーの再起動の時間を早くしたいと思っているような巨大なウェブサイトでもそうであるし、起動時間が減れば作業時間もそのまま減るようなコマンドラインツールの作者からも言われる。
特にBazaarのようなアプリケーションの場合、現在は起動時間はほとんどimportにかかった時間である。Pythonは、インポートを実行時まで遅延させたり、インポートがどのように動作するかをカスタマイズできるようなフックをたくさん提供したり、モジュールを読み込む場所を変更できたり、数多くの柔軟性を提供している。この柔軟性の代償としてimportが遅くなっているのである。
多くのユーザにとっては、特定のサーバで利用されるような柔軟性のメリットはあまりない。起動ごとにimportの動作が変える必要はないのである。そのため、我々はより厳格で動作の速いimport文の選択肢を提供しようと考えている。アイディアのひとつは、必要となるすべてのモジュールを、ひとつにまとめて自己完結したバイナリを格納するという方法である。これは、a)ファイル操作のシステムコールがimportするたびに発生するのを避ける、b)Pythonのレベルでの、リンク時の最適化が行えるようになるというメリットがある。結果として、内部モジュールのインライン化や同様の最適化が行えて、速いコードが手に入るのである。自己完結イメージは特に、サーバアプリケーションの実装をする多くのPythonユーザにとって魅力的である。ビルドとデプロイが効率的に行えるからである。
内部構造の変更が少ない高速化としては、Pythonのmarshalモジュールの最適化が考えられる。marshalモジュールは.pycや.pyoといったファイルの生成で使用されている。Unladen Swallowの作業の成果であるcPickleの高速化の技術を基にすることで、少ない手間でmarshalモジュールの高速化を行うことができる。
いくつかの設定の違いごとの、起動時間を追跡するベンチマークは手元にある。現在のCPythonの起動時間の大きな部分をimportが占めているので、このベンチマークにimportにフォーカスした小さいなベンチマークを追加しようと考えている。
Unladen Swallowのtestsディレクトリの中には、いくつかの面白いパフォーマンステストのディレクトリがある。pref.pyというファイルが我々が開発に利用するベンチマークの主要なインタフェースである。このファイルが、各種ベンチマークの起動の橋渡しをしたり、*.py[co]ファイルの掃除をしたり、実行結果の統計を取ったりするのである。
Unladen Swallowのベンチマーク集は、主要なPythonアプリケーション、特にウェブアプリケーションのホットスポットにフォーカスしている。我々が調査した主要なウェブアプリケーションでは、主にテンプレートシステムがボトルネックであると分かった。そのため、我々の初期のベンチマーク集は以下の項目に注目した。
DjangoとSpitfireのテンプレート: テンプレート言語としてはこの二つは大きく異なる方法で実装されている。 2to3: Python2の文法をPython3に変換する。興味深いのは、ピュアPythonのカーネルがオブジェクトやメソッドディスパッチを多用するプログラムとなっている。 pickle化とunpickle化: 大規模なアプリケーションでは、Pythonのpickleフォーマットをシリアライズに利用したmemcacheの仕組みに依存している。 これとは別に、マイクロベンチマークも用意している。例えば、Nクイーン問題を解くプログラムや、覆面算を解くプログラムや、起動時間のベンチマークなどである。
これらとは別に、Richards, PyStone, PyBenchといったベンチマークもいくつか含んでいる。これらは、Pythonの実装の完全さを確認したり、他のPython実装と比較する目的で入れているだけである。Unladen Swallowでは、これらのベンチマークは実際のアプリケーションや、Pythonの実装方法のパフォーマンスを代表したベンチマークであるとは考えていない。そのため、デフォルトでは、これらのテストは実行されないし、この結果を持って方法を決めることもないのである。
プロジェクトの、長期のパフォーマンスの傾向をグラフ化するために、Unladen SwallowではGoogleの社内標準のパフォーマンス測定フレームワークを利用している。プロジェクトメンバーは定期的に、これを使用したパフォーマンスの最新情報をメーリングリストに投稿するだろう。しかし、個々の変化のテストに使用するのは、Benchmarksのページで説明されている、perf.pyで十分である。
Unladen Swallowでは、実装の正確さを確認するために、Pythonの標準のテストスイートと、Python2.6上でよく知られているいくつかのサードパーティ製のライブラリの両方を使用する。特に、サードパーティ製のC拡張モジュールは、気づかないようなC言語レベルの変化で、すぐに動作しなくなるため、テストに使用している。
JITの実装が前進するにつれて、我々はファジングテストを通常のテスト実行に組み込む予定である。我々が計画しているのは、Victor Stinner氏のFusilというPythonのファジングツールである。これはa)既に存在しており、b)Python上の本当のバグを発見するデモンストレーションを行ったことがある、というのが理由である。
Unladen Swallowでは–jitオプションを設定することで、JIT実行時の動作を制御することができる。例えば、–jit=neverを指定すると、JITは完全に動作停止をする。–jit=alwaysを設定すると、初回のウォームアップのインタプリタの実行をスキップし、直接ネイティブコード生成を行うようになる。–jit=onceを設定すると、再コンパイルしなくなる。これらのオプションは、状況に応じてさまざまな実行方法をテストするのに使用される予定である。我々の目標は、例えば最適化が十分に行われて初めて発生するようなバグのある機能があったとして、そのような決して出会うことのないようなJITバグを見つけることである。これはJVMで提供されているinterpreted modeと同様のものである。
Unladen Swallowでは上記のようなテストをコミットごとに毎回実行するために、BuildBotインスタンスを整備している。
CPythonの美徳のひとつはシンプルさである。CPythonのVMとコンパイラを変更するのも比較的シンプルで簡単であった。LLVMを組み込む作業は、必然的にCPython実装を複雑にしてしまうものである。この追加実装からくる生産的なトレードオフを測定するために、Unladen Swallowチームは定期的にpython-devとpython-ideasというメーリングリストからアイディアをもらい、実装している。もしそのアイディアを実装するのがCPythonに適用させるのよりも難しければ、それをマージする前に何かを修正しなければならないということが分かるのである。チームメンバー以外のメンバーにやってもらいたいのは、バイアスが少ない視点で実装してもらうことである。