Python は使いやすいプログラミング言語ですが、実行速度はお世辞にも速いとは言えません。時間がかかる処理をC言語で記述してライブラリを作成し、それをインポートして呼び出す方法もありますが、Python のプログラムを変更なして高速化できるならば、その方が簡単で便利です。今回は Python のプログラムを高速化する簡単な方法を 2 つ紹介します。
Python は Guido van Rossum 氏がC言語で作成した処理系 (CPython) が標準ですが、これ以外にもいくつか処理系があります。その中で JIT コンパイル機能を持つ PyPy は CPython の数倍速いといわれています。
PyPy は上記公式サイトからダウンロードすることができます。また、少し古いバージョンになりますが、Ubuntu 20.04 LTS の場合、次のコマンドでインストールすることができます。
$ sudo apt install pypy3 $ pypy3 --version Python 3.6.9 (7.3.1+dfsg-4, Apr 22 2020, 05:15:29) [PyPy 7.3.1 with GCC 9.3.0]
PyPy はどのくらい速いのか、たらいまわし関数を使って調べてみました。プログラムは次のようになります。
リスト : たらいまわし関数 (tarai.py) import time def tarai(x, y, z): if x <= y: return y return tarai(tarai(x - 1, y, z), tarai(y - 1, z, x), tarai(z - 1, x, y)) def tak(x, y, z): if x <= y: return z return tak(tak(x - 1, y, z), tak(y - 1, z, x), tak(z - 1, x, y)) def test(func, x, y, z): s = time.time() print(func(x, y, z)) e = time.time() print(e - s)
それでは実行結果を示します。実行環境は Ubuntu 20.04 LTS (WSL1), Intel Core i5-6200U 2.30GHz です。
$ python3 Python 3.8.10 (default, Jun 22 2022, 20:18:18) [GCC 9.4.0] on linux Type "help", "copyright", "credits" or "license" for more information. >>> from tarai import * >>> test(tarai, 14, 7, 0) 14 56.31682109832764 >>> test(tak, 22, 11, 0) 11 61.39163279533386
$ pypy3 Python 3.6.9 (7.3.1+dfsg-4, Apr 22 2020, 05:15:29) [PyPy 7.3.1 with GCC 9.3.0] on linux Type "help", "copyright", "credits" or "license" for more information. >>>> from tarai import * >>>> test(tarai, 14, 7, 0) 14 17.607698678970337 >>>> test(tak, 22, 11, 0) 11 21.849809169769287
PyPy の実行速度は CPython の約 3 倍になりました。プログラムによってはもっと速くなる場合もあるでしょうし、逆に遅くなる場合もあるかもしれません。たとえば、フィボナッチ関数の実行速度は次のようになりました。
リスト : フィボナッチ関数 (tarai.py に追加) def fibo(n): if n < 2: return n else: return fibo(n - 1) + fibo(n - 2) def testfibo(n): s = time.time() print(fibo(n)) e = time.time() print(e - s)
$ python3 Python 3.8.10 (default, Jun 22 2022, 20:18:18) [GCC 9.4.0] on linux Type "help", "copyright", "credits" or "license" for more information. >>> from tarai import * >>> testfibo(38) 39088169 11.766613960266113
$ pypy3 Python 3.6.9 (7.3.1+dfsg-4, Apr 22 2020, 05:15:29) [PyPy 7.3.1 with GCC 9.3.0] on linux Type "help", "copyright", "credits" or "license" for more information. >>>> from tarai import * >>>> testfibo(38) 39088169 1.1087980270385742
最新の PyPy を使うともう少し速くなるかもしれません。興味のある方はいろいろ試してみてください。
PyPy のほかにも、Python のプログラムを JIT コンパイルする方法があります。ライブラリ Numba をインポートすると Python に JIT コンパイラを導入することができます。
Numba のインストールは pip を使うと簡単です。pip は Python で書かれたパッケージ管理ツールです。次のコマンドで pip がインストールされているか確認することができます。
$ python3 -m pip -V /usr/bin/python3: No module named pip
pip が入っていない場合は、次のコマンドでインストールしてください。
sudo apt install python3-pip
あとは pip で Numba をインストールするだけです。
$ pip3 install numba
Numba の基本的な使い方は簡単で、高速化したい関数の前に @jit を付けるだけです。これでその関数が JIT コンパイルされます。それでは、たらいまわし関数で実行時間を計測してみましょう。
リスト : たらいまわし関数 from numba import jit import time @jit def tarai(x, y, z): if x <= y: return y return tarai(tarai(x - 1, y, z), tarai(y - 1, z, x), tarai(z - 1, x, y)) @jit def tak(x, y, z): if x <= y: return z return tak(tak(x - 1, y, z), tak(y - 1, z, x), tak(z - 1, x, y)) @jit def fibo(n): if n < 2: return n else: return fibo(n - 1) + fibo(n - 2) def test(func, x, y, z): s = time.time() print(func(x, y, z)) e = time.time() print(e - s) def testfibo(n): s = time.time() print(fibo(n)) e = time.time() print(e - s)
$ python3 Python 3.8.10 (default, Jun 22 2022, 20:18:18) [GCC 9.4.0] on linux Type "help", "copyright", "credits" or "license" for more information. >>> from tarai import * >>> test(tarai, 14, 7, 0) 14 3.2922072410583496 >>> test(tarai, 14, 7, 0) 14 2.9705183506011963 >>> test(tarai, 14, 7, 0) 14 2.9666898250579834 >>> test(tak, 22, 11, 0) 11 3.4130196571350098 >>> test(tak, 22, 11, 0) 11 3.277550220489502 >>> test(tak, 22, 11, 0) 11 3.268494129180908 >>> testfibo(38) 39088169 0.545701265335083 >>> testfibo(38) 39088169 0.4606921672821045 >>> testfibo(38) 39088169 0.46005892753601074 実行環境 : Ubuntu 20.04 LTS (WSL1), Intel Core i5-6200U 2.30GHz
素の Python3 では tak(22, 11, 0) に 61.4 秒、PyPy3 では 21.8 秒かかりますが、Numba を使用することで 3.3 秒に短縮することができました。初回の実行にはちょっと時間がかかりますが、2 回目以降はどの関数も高速に実行することができました。
JIT コンパイラを使ったスクリプト言語では Julia が速いのですが、たらいまわし関数を実行すると約 2 秒ほどかかります。Julia に迫る速度を叩き出すのですから、Numba の JIT コンパイラは優秀ですね。Python のプログラムを高速化するときには試してみたいツールだと思ました。
>>> def foo(): ... a = yield "foo" ... print("foo: ", a) ... b = yield "bar" ... print("foo: ", b) ... c = yield "baz" ... print("foo: ", c) ... >>> g = foo() >>> print(g.send(None)) foo >>> print(g.send("foo1")) foo: foo1 bar >>> print(g.send("foo2")) foo: foo2 baz >>> print(g.send("foo3")) foo: foo3 Traceback (most recent call last): File "<stdin>", line 1, in <module> StopIteration >>> for x in foo(): print(x) ... foo foo: None bar foo: None baz foo: None
リスト : 複数のコルーチンを呼び出す def make_coroutine(code): while True: print(code, end="") yield None def test1(n): a = make_coroutine("h") b = make_coroutine("e") c = make_coroutine("y") d = make_coroutine("!") e = make_coroutine(" ") for _ in range(n): for g in [a, b, c, d, e]: next(g) for g in [a, b, c, d, e]: g.close()
>>> test1(5) hey! hey! hey! hey! hey! >>> >>> test1(10) hey! hey! hey! hey! hey! hey! hey! hey! hey! hey! >>>
リスト : 子コルーチンから子コルーチンを呼び出す def make_coroutine2(code, g): while True: print(code, end="") if g: next(g) yield None def test2(n): e = make_coroutine2(" ", None) d = make_coroutine2("!", e) c = make_coroutine2("y", d) b = make_coroutine2("e", c) a = make_coroutine2("h", b) for _ in range(n): next(a) for g in [a, b, c, d, e]: g.close()
>>> test2(5) hey! hey! hey! hey! hey! >>> >>> test2(15) hey! hey! hey! hey! hey! hey! hey! hey! hey! hey! hey! hey! hey! hey! hey!
リスト : エラトステネスの篩 # n から始まる整数列 def integers(n): while True: yield n n += 1 # フィルター def stream_filter(pred, s): while True: x = next(s) if pred(x): yield x # n 個の素数を求める def sieve(n): # 変数 x の値をクロージャ内に保持する def make_pred(x): return lambda y: y % x != 0 # nums = integers(2) ps = [] for _ in range(n): x = next(nums) ps.append(x) nums = stream_filter(make_pred(x), nums) return ps # 再帰にすると lambda だけでも動作する def sieve1(n, nums, ps): if n == 0: return ps else: x = next(nums) ps.append(x) return sieve1(n - 1, stream_filter(lambda y: y % x != 0, nums), ps)
>>> sieve(25) [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97] >>> sieve(100) [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97, 101, 103, 107, 109, 113, 127, 131, 137, 139, 149, 151, 157, 163, 167, 173, 179, 181, 191, 193, 197, 199, 211, 223, 227, 229, 233, 239, 241, 251, 257, 263, 269, 271, 277, 281, 283, 293, 307, 311, 313, 317, 331, 337, 347, 349, 353, 359, 367, 373, 379, 383, 389, 397, 401, 409, 419, 421, 431, 433, 439, 443, 449, 457, 461, 463, 467, 479, 487, 491, 499, 503, 509, 521, 523, 541] >>> sieve1(25, integers(2), []) [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97] >>> sieve1(100, integers(2), []) [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97, 101, 103, 107, 109, 113, 127, 131, 137, 139, 149, 151, 157, 163, 167, 173, 179, 181, 191, 193, 197, 199, 211, 223, 227, 229, 233, 239, 241, 251, 257, 263, 269, 271, 277, 281, 283, 293, 307, 311, 313, 317, 331, 337, 347, 349, 353, 359, 367, 373, 379, 383, 389, 397, 401, 409, 419, 421, 431, 433, 439, 443, 449, 457, 461, 463, 467, 479, 487, 491, 499, 503, 509, 521, 523, 541]
リスト : 簡単なマルチプロセス # 中断中のプロセスを格納するキュー proc_queue = [] # キューに追加 def enqueue(x): proc_queue.append(x) # キューからデータを取り出す def dequeue(): x = proc_queue[0] del proc_queue[0] return x # キューは空か? def isempty(): return len(proc_queue) == 0 # プロセスの生成 def fork(fn): g = fn() g.send(None) proc_queue.append(g) # メインプロセス def main_process(*args): # プロセスの登録 for fn in args: fork(fn) # 実行 while not isempty(): p = dequeue() if p.send(False): enqueue(p) else: p.close() # 子プロセス def mtest0(name, n): while n > 0: print(name, n) yield True n -= 1 yield False
>>> main_process(lambda : mtest0("foo", 5), lambda : mtest0("bar", 7)) foo 5 bar 7 foo 4 bar 6 foo 3 bar 5 foo 2 bar 4 foo 1 bar 3 bar 2 bar 1 >>> main_process(lambda : mtest0("foo", 5), lambda : mtest0("bar", 7), lambda : mtest0("oops", 6)) foo 5 bar 7 oops 6 foo 4 bar 6 oops 5 foo 3 bar 5 oops 4 foo 2 bar 4 oops 3 foo 1 bar 3 oops 2 bar 2 oops 1 bar 1
async def name(args, ...): ...
asyncio.run(coro)
await coro
リスト : コルーチンの簡単な例 import asyncio import time async def hello(): print('Hello ...') await asyncio.sleep(1) print('... World!') return True
>>> asyncio.run(hello()) Hello ... ... World! True
asyncio.sleep(delay, result = None)
リスト : 複数のコルーチン (1) async def hello2(): print('Hello2 ...') await asyncio.sleep(2) print('... World2') return 2 async def hello3(): print('Hello3 ...') await asyncio.sleep(3) print('... World3') return 3 async def hello4(): print('Hello4 ...') await asyncio.sleep(4) print('... World4') return 4 async def test00(): s = time.time() a = await hello4() b = await hello3() c = await hello2() print(a, b, c) print(time.time() - s)
>>> asyncio.run(test00()) Hello4 ... ... World4 Hello3 ... ... World3 Hello2 ... ... World2 4 3 2 9.057184934616089
async.create_task(coro) => Task object
リスト : 複数のコルーチン (2) async def test1(): t1 = asyncio.create_task(hello4()) t2 = asyncio.create_task(hello3()) t3 = asyncio.create_task(hello2()) s = time.time() a = await t1 b = await t2 c = await t3 print(a, b, c) print(time.time() - s)
>>> asyncio.run(test1()) Hello4 ... Hello3 ... Hello2 ... ... World2 ... World3 ... World4 4 3 2 4.002448081970215
asyncio.gather(coro, ...) => [result, ...]
リスト : 複数のコルーチン (3) async def test2(): s = time.time() result = await asyncio.gather(hello4(), hello3(), hello2()) print(result) print(time.time() - s)
>>> from async_test import * >>> asyncio.run(test2()) Hello4 ... Hello3 ... Hello2 ... ... World2 ... World3 ... World4 [4, 3, 2] 4.019838809967041
リスト : 複数のコルーチン (4) async def test_sub(name, n): while n > 0: print(name, n) await asyncio.sleep(0) n -= 1 return False async def test3(): result = await asyncio.gather(test_sub("foo", 5), test_sub("bar", 3), test_sub("baz", 7)) print(result)
>>> asyncio.run(test3()) foo 5 bar 3 baz 7 foo 4 bar 2 baz 6 foo 3 bar 1 baz 5 foo 2 baz 4 foo 1 baz 3 baz 2 baz 1 [False, False, False]
リスト : キューの使用例 async def send_color(color, n, queue): while n > 0: await queue.put(color) await asyncio.sleep(0) n -= 1 async def receive_color(n, queue): while n > 0: item = await queue.get() print(item, end=" ") n -= 1 async def test4(): q = asyncio.Queue(4) await asyncio.gather(send_color("red", 9, q), send_color("blue", 7, q), send_color("yellow", 8, q), receive_color(24, q))
>>> asyncio.run(test4()) red blue yellow red blue yellow red blue yellow red blue yellow red blue yellow red blue yellow red blue yellow red yellow red
リスト : 哲学者の食事 # フォークの初期化 def init_forks(): global forks forks = [True for _ in range(5)] # フォークの番号を求める def fork_index(person, side): return person if side == 'right' else (person + 1) % 5 # フォークがあるか def isFork(person, side): return forks[fork_index(person, side)] # フォークを取る async def get_fork(person, side): while True: if isFork(person, side): forks[fork_index(person, side)] = False break await asyncio.sleep(1) await asyncio.sleep(1) return True # フォークを置く async def put_fork(person, side): forks[fork_index(person, side)] = True await asyncio.sleep(1) return True # 哲学者の動作 (デッドロック) async def person0(n): for _ in range(2): print(f'Philosopher {n} is thinking') await get_fork(n, 'right') await get_fork(n, 'left') print(f'Philosopher {n} is eating') await asyncio.sleep(1) await put_fork(n, 'right') await put_fork(n, 'left') print(f'Philosopher {n} is sleeping') async def test50(): init_forks() ps = [person0(n) for n in range(5)] await asyncio.gather(*ps)
>>> asyncio.run(test50()) Philosopher 0 is thinking Philosopher 1 is thinking Philosopher 2 is thinking Philosopher 3 is thinking Philosopher 4 is thinking ^C
リスト : デッドロックの解消 async def person2(n): for _ in range(2): print(f'Philosopher {n} is thinking') if n % 2 == 0: await get_fork(n, 'right') await get_fork(n, 'left') else: await get_fork(n, 'left') await get_fork(n, 'right') print(f'Philosopher {n} is eating') await asyncio.sleep(1) await put_fork(n, 'right') await put_fork(n, 'left') print(f'Philosopher {n} is sleeping') async def test52(): init_forks() ps = [person2(n) for n in range(5)] await asyncio.gather(*ps)
>>> asyncio.run(test52()) Philosopher 0 is thinking Philosopher 1 is thinking Philosopher 2 is thinking Philosopher 3 is thinking Philosopher 4 is thinking Philosopher 0 is eating Philosopher 3 is eating Philosopher 0 is thinking Philosopher 1 is eating Philosopher 3 is thinking Philosopher 0 is eating Philosopher 1 is thinking Philosopher 2 is eating Philosopher 4 is eating Philosopher 0 is sleeping Philosopher 2 is thinking Philosopher 1 is eating Philosopher 4 is thinking Philosopher 3 is eating Philosopher 1 is sleeping Philosopher 2 is eating Philosopher 3 is sleeping Philosopher 4 is eating Philosopher 2 is sleeping Philosopher 4 is sleeping
async for item in agen: ...
await agen.send(value) => item
リスト : 非同期ジェネレータの簡単な使用例 import asyncio # 非同期ジェネレータ async def async_gen(): a = yield "foo" print(a) await asyncio.sleep(0.5) b = yield "bar" print(b) await asyncio.sleep(0.5) c = yield "baz" print(c) async def test0(): xs = [] async for x in async_gen(): xs.append(x) return xs async def test1(): return [x async for x in async_gen()] async def test2(): g = async_gen() try: a = await g.asend(None) print(a) b = await g.asend(1) print(b) c = await g.asend(2) print(c) d = await g.asend(3) print(d) except StopAsyncIteration: pass return [a, b, c] async def test3(): g = async_gen() try: a = await g.__anext__() print(a) b = await g.__anext__() print(b) c = await g.__anext__() print(c) d = await g.__anext__() print(d) except StopAsyncIteration: pass return [a, b, c]
>>> asyncio.run(test0()) None None None ['foo', 'bar', 'baz'] >>> asyncio.run(test1()) None None None ['foo', 'bar', 'baz'] >>> asyncio.run(test2()) foo 1 bar 2 baz 3 ['foo', 'bar', 'baz'] >>> asyncio.run(test3()) foo None bar None baz None ['foo', 'bar', 'baz']