2008-04-06

Python iterative operation benchmark

Python、嫌いです、確定していてさえ実行時まで型をバインドしないし、遅いし、歴史的な負の遺産をそのまま引きずってるし、言語思想がレガシーだから。でもしばらく使い続けることになるでしょう。以下は、書き手が気を抜くといかに Python が遅く動作するかを示すコード。

  1. import gc
  2. import time
  3.  
  4. def benchmark(test):
  5.     "Benchmark runner"
  6.     gc.collect()
  7.     t1 = time.time()
  8.     test()
  9.     t2 = time.time()
  10.     print "%.3f" % (t2-t1)
  11.     gc.collect()
  12.  
  13. def test1():
  14.     list = ["a" for _ in range( 0, 10000000 )]
  15.     s = "".join(list)
  16.  
  17. def test2():
  18.     generator = ("a" for _ in range( 0, 10000000 ))
  19.     s = "".join(generator)
  20.  
  21. def test3():
  22.     "handling s will cause a lot of memory operations. worst case"
  23.     s = ""
  24.     for _ in range( 0, 10000000 ):
  25.         s += "a"
  26.  
  27. def test4():
  28.     "same as test2"
  29.     s = "".join("a" for _ in range( 0, 10000000 ))
  30.  
  31. def test5():
  32.     "use xrange istead of range. xrange return generator instead of list."
  33.     s = "".join("a" for _ in xrange( 0, 10000000 ))
  34.  
  35. def test6():
  36.     "built-in fastest operation"
  37.     s = "a" * 10000000

実行

  1. benchmark(test1)
  2. benchmark(test2)
  3. # benchmark(test3) # Out-of-range!
  4. benchmark(test4)
  5. benchmark(test5)
  6. benchmark(test6)

実行結果ですが、

予想通りの数値(横軸は秒で、短い方が速いことを意味する)。たとえば test1 では list と range の2度にわたってリスト化が発生しており、このせいで一時的なメモリ使用量が増えてしまい性能が悪くなる。test2 または test5 のように、ジェネレータに置き換えるだけでメモリ効率が改善される(※1)。それでも C で実装された '*' 演算 (test6) には到底かなわない。これが意味するところは、普通のコードだと思ってさらっと書いたコードが実は最速の場合の 265 倍もの実行時間を要するということ。ランタイムの構造・性格や利用するデータ構造を正確に理解していないと適切な最適化は難しいので注意が必要。.NET のように Reflector でコンパイルされたコードのコストを分析したりはできないのだ。

IronPython 1.1.1の場合。いつの間にか CPython の方が性能が良くなっているらしい。確か IronPython が速いと言われていた頃は Python 2.3 とかその辺りだったような。

ではまあDLRを装備した 2.0B1で試してみるか、、、さらに性能が悪くなっている。まだ正式リリース版ではないから何とも言えないがDLRを切りだすためにコストを払っているのではないかと疑りをかけずにはいられない。ちょっと CLR に胡坐をかきすぎじゃないのか?

※1 test1, test2 だけで比較すると、IronPython ではジェネレータにした方が速くなっているが、CPython では若干遅くなっている。CPython はリスト処理に対してケースバイケースの最適化を施しているのではなかろうか。調べてみる必要がある。

※2 ちなみに test3 は数分以内に停止せずメモリの使用量も半端ではなかったので結果から外した。興味深いのはこれを Linux の CPython 2.5.1 で実行すると test1, test2 程度の時間で処理されることだ。文字列演算 '+=' の実装が Windows とはまったく異なることが予想される(.NET でいえばStringBuilder 的な実装になっているのではないか?)。

0 コメント: