PythonSpeed

多くの人がPythonプログラムの速度について心配を持っています。でもPythonを使わないと、堪らないくらい実行速度上のロスがありますよね? 中には「なんだ、インタプリタのスクリプト言語か、まるっきり遅いや」なんて結論づける人もいます。また、Pythonを実際に試してみて、実行効率が十分なことに気づく人もいます。でも時には、 とっても遅いプログラムができあがることもあります。

実行速度がそんなに重要?ホントに?

多くの人が必要以上に速度に取りつかれていて、このような種類の問題では、Cが優れた実績を示していることから、全ての面で優れた言語だと考えています。別の人々は、開発の速度がより重要で、Pythonを選ぶのはそのような時に限り、まあそれなりの速度だろうと考えています。そして頻繁に、期待を超えた速度で動いていることに驚かされています。時には、同じ開発時間を費やしたC/C++のコードより速いことさえあります。

一般的に重要とされてるのは絶対的な速度ではありません。これは実行可能ファイルへのアクセス速度について考えてみば分かると思います。このアクセス速度をもたらしている機構を超えた最適化は、リソース(普通はあなたの時間、つまりお金)の無駄でしょう。

速度と拡張性を改善するテクニック

最もよい実行効率を提供する、コーディングのガイドラインがいくつか存在します。

一番良いアルゴリズムと最速のツールを使う

  • setsや辞書を使ってメンバをテストする、O(1)のような形式は、検索を行なうO(n)という形式よりも高速です。"a in b"を調べるとき、bはリストやタプルではなくsetsや辞書であるべきです。
  • 文字列連結は`''.join(seq)`を使うのがベストです。 これはO(n)のプロセスで行われます。対照的に、演算子'+' や '+='を使うとO(n**2)のプロセスがかかります。これは演算の途中で新たな文字列型変数が生成されるからです。CPython 2.4のインタプリタはこの制限をいくらか緩和していすまが、`''.join(seq)` が最良の手段であることに変わりありません。
  • 多くのツール(rangeやxrange、mapやitertools.imap、リストの内包表記とジェネレータ、dict.itemsとdict.iteritems)でリストとイテレータの両方が提供されています。一般的に、イテレータはより省メモリでスケーラビリティに優れています。これらは、リストそのものが必要とされない場合によく使われています。
  • (CPythonの)コアとなる構成要素のいくつかは、最適化されたCのコードで書かれています。このアドバンテージを利用するアプリケーションは、実質的な実行効率の向上を得ることができます。この組み込みの構成要素にはすべての組み込み型(リスト、タプル、sets、辞書)と、array、itertools、collection.dequeのような拡張モジュールを含みます。
  • 同様に、組み込み関数は手で書かれたコードより速く動作します。例えばmap(operator.add, v1, v2)はmap(lambda x,y: x+y, v1, v2)より速く動きます。
  • リストは固定長の配列と可変長のスタックより速く動作します。しかし、pop(0)またはinsert(0,v)、collection.deque()を使うアプリケーションはO(1)という高い効率を発揮します。これはそれぞれの追加、削除するごとO(n)回におよぶ、リストの再構築を避けられるからです。
  • カスタムの順序ソートでは、Python2.4のkey=オプションあるいは伝統的なdecorate-sort-undecorateテクニックを使うと、もっとも良い効率が得られます。両者のアプローチとも要素ごとに1度だけkey関数を呼びます。対照的にソートのcmp=オプションは、ソートを行う間に要素ごと何度も呼ばれます。例えば、sort(key=str.lower)はsort(cmp=lambda a,b: cmp(a.lower(), b.lower())) より速いのです。

インタープリタの最適化による利点

  • 関数の中で、ローカル変数はグローバル変数、組込み定数、属性の参照より高速にアクセスされます。そのため、内部ループの中に局所化したローカル変数へアクセスするより遅くなることがあります。例えば、 random.shuffle()というコードを random=self.random という1文で局所化します。これによってshuffleのループがself.ramdomを何度も参照するのを防ぎます。ループの外側での向上率は非常に小さいため、その対価を得ることはめったにありません。
  • 次に推奨するのは、定数を表す式をループの外側に置くという規則を一般化することです。同様に、組み合わせによる定数はあらかじめ計算しておくことが必要です。ループの中では"x=1+2"と書く代わりに"x=3"としましょう。
  • 関数呼び出しのオーバーヘッドはその他の命令に比べて大きくなります。そのため、実行速度の求められるループの中でのインライン構文はより効率が悪くなることがあります。
  • リストの内包表記は同様のforループに比べて僅かに早く動作します。
  • Py2.3以降を使えば、コンパイラが"while 1"を単一のジャンプに最適化します。対照的に"while True"にはもう何ステップか必要とします。後者が明快に動作するのであれば、速度の必要なコードにはひとつめの形式を用いると良いでしょう。
  • 複数ターゲットへの代入は単一の代入より遅くなります。つまり "x,y=a,b" は "x=a; y=b"より遅くなります。一方で、複数ターゲットへの代入は変数の交換より早く動作します。つまり "x,y=y,x" は"t=x; x=y; y=t" より早く動作します。
  • 連続した比較文は "and" 演算子より早く動作します。"x < y and y < z"と書く代わりに"x < y < z"としましょう。
  • もう幾つかのアプローチがハックとされ、要求の多いアプリケーションのためにのみ残されています。例を挙げると "not not x" は "bool(x)"とするより速く動作します。

診断ツールの長所を活かそう

  • hotshotとprofileモジュールは実行効率上のボトルネックを特定するのに役立ちます。profileはピュアPythonコードとCコードそれぞれが費やす時間を区別できます。
  • timeitモジュールはコード内の独立した1行とその代替手段との実行効率の比較を、迅速に行なう機能を提供します。

実行効率が全体の方針を決定できる

  • XMLの処理においてSAXは一般的にDOMを使うより速く、メモリ効率も良くなります。
  • スレッドの利用はI/Oの待機によって時間を浪費するようなアプリケーションの応答時間を改善できるでしょう。
  • selectモジュールは複数のソケットをプーリングすることによるオーバーへッドを最小化する助けとなるでしょう。

実行効率を高める標準外のツールを検討する

  • Numpy はとても豊富な数値計算に必須のツールです。
  • Psycoとpyrexはネイティブコードの速度に迫る助けになるでしょう。

さらなる実行効率のTips

さらなる実行効率に関するTipsと例をPythonSpeed/PerformanceTips で見つけられます。


翻訳について

この文書は http://www.python.org/cgi-bin/moinmoin/PythonSpeed で公開されているPythonの実行効率に関するエッセイ“PythonSpeed”の翻訳です。元々はIrmen De Jong氏が自身のmoinmoinで公開した文章でしたが、Daily Python URLで紹介されて評判になったのか、その後Python.orgのWikiに転載されています。

追記(2005-05-21)

PyCon2005の終了後にPython.orgのWiki全体に大きく手が入れられ、それに伴いこの文書も改訂されました。翻訳の方もようやく改訂に追従できましたが、訳には怪しい場所もあると思いますので、お気づきの点はご連絡いただけるとうれしいです。 -- turky

さらに追記(2006-09-25)

元々、このページは編集不可にしていたのだけれど、このところSPAMコメントが多いので、WikiからReSTのページに移動することにしました--turky

さらにさらに追記(2007-09-20)

誤訳の指摘に今さら気づいたため、訳文を少しだけ修正しました。西尾さん、ありがとうございます。