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

  • 244
    いいね
  • 0
    コメント

「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)