322
289

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Last updated at Posted at 2014-03-24

「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)
322
289
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
322
289

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?