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


「Pythonの遅い部分をCで書き直すと実行速度が100倍になりました！（神奈川県・主婦・30代）」といった広告をよく週刊誌で見かける。しかし、型ゆとり世代にとってCはいささかハードルが高い。一方Python並列化ならば追加の手間はかなり少なくて済み、100倍とは言わないが数倍程度の高速化ができる。

# 並列化する計算

言うまでもないが、HTTP通信が律速になっているようなPythonでは並列化しても高速化されない。並列計算を要するのは大体、巨大なforループ計算である。例として以下のようなものを考える。

```python
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モジュールによる並列化

まず部分和を計算するための関数を定義する。

```python
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` モジュールを使う。

```python
#!/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](https://qiita-image-store.s3.amazonaws.com/0/40166/d04ce4ed-7e4d-ac85-2704-e58c5de44b22.png "proc-time.png")

CPUが物理4コアであるため、4個まではほぼコア数に比例して加速するが、5並列にすると4コアのうち1個のみ2プロセスを実行する事になるため、その段階が律速になり、4並列のときよりも計算時間が増えてしまう。
このような事情もあって、最速は8並列の場合である。

他のCPUでも、恐らく仮想コア数の数に合わせるのが最速と思われる。

# その他

`mp.Pool()`を用いた並列化はシンプルに記述できるが、細かい制御ができない点や、pickle化できない関数には適用できないらしい（classのmethodなどを並列化したいときなどに問題になるらしい）という欠点がある。

より複雑な並列処理をしたい場合、`mp.Process()`で個別にプロセスを作成して実行し、`mp.Queue()`でデータのやりとりをするといった方法もある。

以下にサンプルを載せる。各プロセスが`queue`にデータを送り、それをマスターが受け取るという形になっている。

```python
#!/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)
```