「Pythonの遅い部分をCで書き直すと実行速度が100倍になりました!(神奈川県・主婦・30代)」といった広告をよく週刊誌で見かける。しかし、型ゆとり世代にとってCはいささかハードルが高い。一方Python並列化ならば追加の手間はかなり少なくて済み、100倍とは言わないが数倍程度の高速化ができる。
並列化する計算
言うまでもないが、HTTP通信が律速になっているようなPythonでは並列化しても高速化されない。並列計算を要するのは大体、巨大なforループ計算である。例として以下のようなものを考える。
L = 20000
total = 0
for i in range(L):
for j in range(L):
total += i*j
print (total)
手元のマシンでの実行時間は25.663秒。これを並列化によって高速化する。
Thread並列とProcess並列
並列化は主にThread並列(メモリ共有)とProcess並列(非共有)の2方式があり、Pythonではそれぞれのモジュールが提供されているが、諸般の事情 (Global Interpreter Lock) により、Pythonでは Thread並列化による高速化はできない。したがって本稿ではProcess並列について述べる。
Process並列では、それぞれ独立したメモリ領域で計算が行われるため、同一のtotal
変数にアクセス出来ない。従って上のような総和計算では
- 計算範囲を分割する
- それぞれの部分和を計算する
- それらの総和をとる
という手順になる。
multiprocessingモジュールによる並列化
まず部分和を計算するための関数を定義する。
L = 20000
proc = 8 # 8並列とする
# 各プロセスが実行する計算
def subcalc(p): # p = 0,1,...,7
subtotal = 0
# iの範囲を設定
ini = L * p / proc
fin = L * (p+1) / proc
# 計算を実行
for i in range(ini, fin):
for j in range(L):
subtotal += i * j
return subtotal
次にこれらを並列で実行する。
ここでは multiprocessing
モジュールを使う。
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import multiprocessing as mp
L = 20000
proc = 8 # 8並列とする
# 各プロセスが実行する計算
def subcalc(p): # p = 0,1,...,7
subtotal = 0
# iの範囲を設定
ini = L * p / proc
fin = L * (p+1) / proc
# 計算を実行
for i in range(ini, fin):
for j in range(L):
subtotal += i * j
return subtotal
# 8個のプロセスを用意
pool = mp.Pool(proc)
# 各プロセスに subcalc(p) を実行させる
# ここで p = 0,1,...,7
# callbackには各戻り値がlistとして格納される
callback = pool.map(subcalc, range(8))
# 各戻り値の総和を計算
total = sum(callback)
print (total)
手元のPC (Intel Core i7 870) は物理4コア・仮想8コアであるため、8プロセス並列とした。
計算時間は 6.235秒 で、4倍以上高速化された。
並列数と速度
計算時間はこのようになった。
CPUが物理4コアであるため、4個まではほぼコア数に比例して加速するが、5並列にすると4コアのうち1個のみ2プロセスを実行する事になるため、その段階が律速になり、4並列のときよりも計算時間が増えてしまう。
このような事情もあって、最速は8並列の場合である。
他のCPUでも、恐らく仮想コア数の数に合わせるのが最速と思われる。
その他
mp.Pool()
を用いた並列化はシンプルに記述できるが、細かい制御ができない点や、pickle化できない関数には適用できないらしい(classのmethodなどを並列化したいときなどに問題になるらしい)という欠点がある。
より複雑な並列処理をしたい場合、mp.Process()
で個別にプロセスを作成して実行し、mp.Queue()
でデータのやりとりをするといった方法もある。
以下にサンプルを載せる。各プロセスがqueue
にデータを送り、それをマスターが受け取るという形になっている。
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import multiprocessing as mp
L = 20000
# 各プロセスが実行する計算
def subcalc(queue, p):
subtotal = 0
# iの範囲を設定
ini = L * p / proc
fin = L * (p+1) / proc
for i in range(ini, fin):
for j in range(L):
subtotal += i * j
# キューにデータを送る
queue.put(subtotal)
# キューを作成
queue = mp.Queue()
# 8個のプロセスを用意
ps = [
mp.Process(target=subcalc, args=(queue, 0)),
mp.Process(target=subcalc, args=(queue, 1)),
mp.Process(target=subcalc, args=(queue, 2)),
mp.Process(target=subcalc, args=(queue, 3)),
mp.Process(target=subcalc, args=(queue, 4)),
mp.Process(target=subcalc, args=(queue, 5)),
mp.Process(target=subcalc, args=(queue, 6)),
mp.Process(target=subcalc, args=(queue, 7))
]
# すべてを開始
for p in ps:
p.start()
# キューから結果を回収
total = 0
for i in range(8):
total += queue.get() # キューに値が無い場合は、値が入るまで待機になる
print(total)