このBlogは移転しました。今後は aish.dev を御覧ください。

Pythonの高速化?

たまに、「Pythonの高速化」なんてブログを見かけることがある。書いてあるのは、たいてい

s = 0
for i in list_of_ints:
   s += i

と書くより、

s = sum(list_of_ints)

のほうが速い!なので sum() 使おう!とかだ。

たしかに、sum() は速い。Pythonインタープリタによるループの繰り返しを行わず、高速なC言語による処理が行われるためだ。特に、リストやタプルが引数に指定されている場合、さらに高速に実行できるように特別扱いされている。

100,000,000個の整数のリストで処理時間を計測してみると、for ループ版では 3.5秒、sum() 版では 2.2秒となった。すごい、2/3になった!forループ糞だな!

…どうだろう…

まず、こういったサンプルプログラムは、極端に単純化されている。私はもう数十年に渡ってさまざまなプログラムを書き続けて来たが、「一億件の整数を加算する」だけのプログラム、というのにお目にかかったことがない。

ふつうは、まず一億件のデータはかならずどっかから取得しなければならない。通常はデータベースやテキストファイルなどにアクセスし、読み込む必要がある。

整数データとはいえ、一億件を普通に読み込めばざっと10秒ぐらいはかかるだろう。ということは、加算の処理で1.3秒節約すると、13.5秒が12.2秒になってそれはそれで意味のある最適化ではあるが、全体としてみればそれほど目覚ましい成果ではない。

こういったケースでは、例えば入力データがテキストデータなら、事前に入力ファイルを分割しておいてデータの読み込みと計算を多重化する、などの手段が有効で、もっと大きな成果を期待できる。

RDBからの読み込みであれば SELECT sum(xx) FROM ... のようにRDBの集計機能を利用すれば、一秒以下で計算できるだろう。

Pythonの書き方を工夫するしてパフォーマンスを改善する、というのは、結局は同じアルゴリズムを、ちょっと無駄の少ない手順でやる、ということに過ぎない。当然、処理時間に多少の差が出ることはあっても、それほど大きな差にはならない。

sum() を使った加算が手作りのループより約3割速くなる、というのが、よく使われる最適化では最も効果の大きい例だが、これもせいぜい数%程度の差しか生じないだろう。

昔は、文字列の結合は

s = ''
for c in words:
    s = s + c

と書くと O(N**2) の時間が必要だが、

s = ''.join(words)

なら激速い、なんてのもあった。しかし、これはかなり昔、ほぼ同等な処理時間となるように最適化されてしまった。

ということで、Pythonでプログラムを書く時には、「パフォーマンスが出る書き方」ではなく、「読みやすい書き方」を心がけよう。

パフォーマンス重視で書いても、そんなには速くはならない。

for 文を使ったほうが読みやすい、と思ったら、迷わずに for 文を使えばよい。

単純なケースなら、sum() のほうが読みやすいだろう。そういった場合は容赦なく sum() を使おう。

読みやすく書いて、全体のアルゴリズムを改善しやすくすること心がけよう。

時間は、ボトルネックの抽出とその解消に使おう。

こまごまとしたチューニングに時間と体力を消費しないようにしよう。

ボトルネックのチューニングを実施した後、最後のダメ押しで for 文の除去などのチューニングを加えるのはもちろん問題ないが、それは一番最後に行うことで、最初から考えるようなことではないのである。