ページ

2011年12月13日

pypyのスレッドとかgreenletとか何か

pypy advent calendarのエントリです。ごめんなさい。ネコを飼うのでそのことに関心が向いて、pypyのこと、すっかり忘れていました。なので、いつにも増して、とってもうすーいエントリをだらだらと書きます。pypy闇の軍団長兼会長様、許してください。

まずは、昔のフィボナッチ数列の計算するコードを持ち出します。
import time

def fibonacci(n):
    value = 0
    f1 = 1
    f2 = -1
    for i in range(n+1):
        value = f1 + f2
        f2 = f1
        f1 = value
    
    return value

if __name__ == "__main__":
    for x in range(3):
        fibonacci(3)
    time.sleep(0)
    start = time.time()
    for x in range(4000, 5000):
        fibonacci(x)
    end = time.time()
    print end - start

これを実行すると、僕のMacBook Airだと今日は大体1.11秒です。Java(Jython?)などのちゃんとしたスレッドを持ってる人たちは、ループの中をスレッドにするともっと高速になるんじゃないか?って考えます。なので、スレッドにしちゃいましょう。
import time
import threading

def fibonacci(n):
    value = 0
    f1 = 1
    f2 = -1
    for i in range(n+1):
        value = f1 + f2
        f2 = f1
        f1 = value
    
    return value

if __name__ == "__main__":
    for x in range(3):
        fibonacci(3)
    time.sleep(0)
    thread_list = []
    start = time.time()
    for x in range(4000, 5000):
        thrd = threading.Thread(target=fibonacci, args=(x,))
        thread_list.append(thrd)
        thrd.start()
    for thrd in thread_list:
        thrd.join()
    end = time.time()
    print end - start

さて、これを実行すると1.7秒ぐらいです。参考までにCPythonで実行すると2.01秒でした。さらに参考までにPython3で実行すると2.37秒でした。Python3ダメダメじゃん。
(;゜〇゜)はっ!これはPython3のエントリじゃないからこれ以上Python3のことを書いたら団長に怒られる!
さて、スレッドにした方が遅いですね。まあ、何も不思議じゃなくって、GILとかあるので、想定の範囲内です。ちなみに、1000個スレッドがあると、LinuxやMacはメモリは見かけ上8Gぐらいなります。まあ、単純な計算だけならスレッドにするより何も考えずにシーケンシャルに行った方が良いと言うことですね。もしくはプロセスのフォークで・・・。でも、途中でネットワークとかブロックするような処理があると、シーケンシャルにやると遅くなっちゃいます。

ここまでが前置きです。結論はないです。Application-level Stackless featuresにcontinuletとgreenletについて書かれています。どっちもマイクロスレッドですね。continuletのレイヤの上にgreenletと同じAPIのモジュールを実装しているようです。32bit環境ではスレッドあたり4KBのメモリ使用量だって書いています。本物のスレッドに比べると遙かに少ないです。
と言うことで、こんなコードを書いてみました。
import time
from greenlet import greenlet

def fibonacci(n):
    value = 0
    f1 = 1
    f2 = -1
    for i in range(n+1):
        value = f1 + f2
        f2 = f1
        f1 = value

    return value

if __name__ == "__main__":
    parent = greenlet()
    start = time.time()
    grs = [greenlet(fibonacci, parent) for _ in range(1000)]
    i = 4000
    for gr in grs:
        gr.switch(i)
        i += 1
    end = time.time()
    print end - start
実行すると1.2秒ぐらいです。最初のコードが1.1秒ぐらいなので0.1秒ぐらいのオーバーヘッドですね。まあ、待ちがないマイクロスレッドでスレッドの途中でコンテキストの切り替えがないので、つまんないですね。

ちょっとこれはいろいろやり過ぎているので、もっと単純なコードでやってみましょう。
import threading
import time


def test2():
    pass

start = time.time()
thrds = [threading.Thread(target=test2) for i in range(1000)]
for thrd in thrds:
    thrd.start()
for thrd in thrds:
    thrd.join()
end = time.time()

print end - start

単にスレッドを作って空の関数を実行しているだけです。これだけのコードで0.29秒です。感想としては、遅い・・・。CPython2.7だと0.14秒です。うっ、pypy負けてる。と言うかJITがうまく動かなさそうなコードなのでいいです。ちなみにPython3でも0.14秒ぐらいでした。
それではgreenletで。
from greenlet import greenlet
import time

def test1(x, y):
    for gr in grs:
        z = gr.switch(2)

def test2(u):
    #print id(greenlet.getcurrent())
    gr1.switch(42)

start = time.time()
gr1 = greenlet(test1)
grs = [greenlet(test2) for x in range(1000)]
gr1.switch(1, 2)
end = time.time()

print end - start

このヒトを実行すると0.07秒です。スレッド重いですね。

0 件のコメント: