IronPythonでマルチスレッドプログラミング + Python3.2RC2のGIL
PythonでのマルチスレッドプログラムとGIL
C言語とかJavaでマルチスレッドプログラムを組んでからCPython(C言語のPython実装)でマルチスレッドプログラムを組むとつまずきます。並列プログラムで実行速度が上がらないからです。
その原因は、Pythonインタプリタがスレッドセーフでないためで、スレッドはインタプリタ全体のロック(Global Interpreter Lock)を取得し排他的に実行されるため、同時に実行可能なスレッド数は1に制限され*1 *2、さらにスレッド切替えのコストが加わります。RubyのバーチャルマシンのYARVも同様にGVLというロックでスレッド数を1に制限しているようです*3。
I/Oがたくさんあるとか、スレッドそれぞれが外部プロセスで並列動作するソフトを起動するとかであれば問題ないのかもしれません。例えばImageMagickのconvertコマンドで大量の画像をサムネイル化するとか、ffmpegで一斉にエンコードするとか。試してないけど。どっちにしろCPUを使うプログラムだとうれしくない。
GILフリーな処理系としてのIronPython
PythonにはCPython, Jython, IronPython, PyPyなどいくつかの実装があります。そのうちの一つ、IronPythonはMicrosoftのCodePlexで公開されているPython処理系で、コードは.NET Framework上で実行されます*4。注目すべきは、.NETにはGILにあたるもの無く制約を受けないところ。
そこで、IronPythonでマルチスレッドのプログラムを書くとどの程度速くなるのかコードを書いて確かめてみました。
ちなみに、将来的にはPythonからGILを除去される方向らしく、Python3.2*5では、GILは残ったものの、これまでのアルゴリズムよりもオーバーヘッドが少ないアルゴリズムが実装されました。*6 *7 *8
今確認したら2011/01/31にRC2がリリースされていました。これでも試してみよう。
IronPythonのインストール
codeplexのサイトから処理系をインストールします。
http://ironpython.codeplex.com/
比較
長さ100800のリストのそれぞれの要素で多項式を計算して返すプログラムを単純に1から8スレッドで分割、計算して比較しました。
環境はCore i5 U430 (1.2GHz), 4GB RAM, Win7Homeです。HTで疑似4コア.
C:\Users\hal>C:\Python32\python.exe x.py 1 0.2040 # スレッド数 実行時間 2 0.2140 3 0.2160 4 0.2060 5 0.2130 6 0.2180 7 0.2150 8 0.2130 C:\Users\hal>C:\Python26\python.exe x.py 1 0.2680 2 0.5110 3 0.3300 4 0.3680 5 0.3230 6 0.3240 7 0.3320 8 0.3780 C:\Users\hal>ipy -V PythonContext 2.6.10920.0 on .NET 4.0.30319.1 C:\Users\hal>ipy x.py 1 11.4817 2 6.2073 3 5.1353 4 4.3832 5 4.5703 6 4.7063 7 4.5713 8 4.6953
Python2.6がスレッドを増やすと性能が劣化する一方で、IronPythonはスケールしているのがわかります。i5はHTで疑似4コアなので上がり方は予想通り。しかし1スレッドあたりの性能が低く、4スレッドでなお1スレッドのPython2.6に負けてしまっています。Python3.2はスレッド増やしてるのに速度比変わらず。なんかすごい。
結論
- IronPythonはGILが無いからスケールするけど・・・・しょぼい。.netだからなのかそれとも吐くコードがしょぼいだけなのか・・・
→ GILがあってもCPythonで。
こ、こんなはずでは・・・でもコア数が増えればIronPythonのが速くなりそうな感じはする。
コード
import threading import time import sys try: range = xrange # xrange is gone in py3 except: pass def g(tid, nt, ary): #print "thread %d: start" % tid n = len(ary) // nt b = n * tid e = b + n for i in range(b, e): x = ary[i] ary[i] = x**3 + 0.1*x + 5 def parallel_g(size, nproc): ary = list(range(size)) s = time.time() ths = [threading.Thread(target=(lambda i: lambda: g(i, nproc, ary))(i)) for i in range(nproc)] [t.start() for t in ths] # start [t.join() for t in ths] # join e = time.time() return e-s # import fractions # lcm = lambda x,y: (x*y) / fractions.gcd(x, y) # reduce(lcm, range(1, 9)) * 120 # => 100800 s = 100800 for n in range(1, 9): sys.stdout.write("%d %.4f\n" % (n, parallel_g(s, n))) # print is function in py3
*1:http://docs.python.org/c-api/init.html#thread-state-and-the-global-interpreter-lock
*2:GlobalInterpreterLock, PythonInfo Wiki
*3:Rubyのスレッディングとガベージコレクションの今後 - 笹田耕一氏インタビュー InfoQ
*4:http://wiki.python.org/moin/IronPython
*5:http://dpo.gbrandl.de/whatsnew/3.2/
*6:Inside the New GIL, David M. Beazley
*7:動的言語総まとめ : PythonのGILが徹底改善されるも廃止はされない。SqueakがAndroidに移植された。, InfoQ