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などいくつかの実装があります。そのうちの一つ、IronPythonMicrosoftCodePlexで公開されている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/

おわり。必要なら環境変数Pathにインストールディレクトリを追加します。

比較

長さ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