Edited at

雨の中、Cを書かずにPythonで並列計算をする人間がいてもいい。自由とはそういうものだ。

More than 3 years have passed since last update.

「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倍以上高速化された。


並列数と速度

計算時間はこのようになった。

proc-time.png

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)