はじめに
ある処理について、複数回の実行を要する場面があります。このとき、処理間の前後関係に制約がない場合は、繰り返し処理(for文やwhile文)や並列処理がその実現方法として挙げられます。今回は、繰り返し処理と並列処理の簡単な比較として、コードと実行時間の違いについて紹介します。
以降に挙げる例はGoogle Colaboratory1で実行しています。
複数回の実行を要する処理の例
import random
import time
def process(num):
"""
本記事におけるある処理。
ある程度の時間がかかる処理と仮定し、1msの一時停止をした後にランダムな数値を出力する。
"""
time.sleep(0.001)
random.seed(num)
return num, random.random()
繰り返し処理の例
%%timeit
if __name__ == "__main__":
nums = range(1, 1_001)
results = []
for num in nums:
result = process(num)
results.append(result)
1.13 s ± 7.37 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
並列処理の例
今回、並列処理はconcurrent.futures.ProcessPoolExecutor
2というクラス(以降、ProcessPoolExecutor
)により実現させます。
今回の実行環境において、利用可能なCPUのコア数は2となります。
import os
os.cpu_count()
2
ProcessPoolExecutor
で指定する並列数は、そのまま利用可能なCPUのコア数(2)とします。
%%timeit
import concurrent.futures
if __name__ == "__main__":
nums = range(1, 1_001)
results = []
with concurrent.futures.ProcessPoolExecutor(max_workers=os.cpu_count()) as executor:
futures = [executor.submit(process, num) for num in nums]
for future in concurrent.futures.as_completed(futures):
if future.exception():
raise Exception(f"{future.exception()}")
else:
results.append(future.result())
702 ms ± 17.1 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
実行時間について、繰り返し処理では平均で1.13msでしたが、並列処理の場合は平均で702msでした。つまり、今回の例においては、繰り返し処理を並列処理に書き換えることによって、実行時間の短縮という恩恵が得られます。
今回の例に基づいて、繰り返し処理から並列処理に書き換える場合の差分は以下となります。
+ import concurrent.futures
+
+
if __name__ == "__main__":
nums = range(1, 1_001)
results = []
- for num in nums:
- result = process(num)
- results.append(result)
+ with concurrent.futures.ProcessPoolExecutor(max_workers=os.cpu_count()) as executor:
+ futures = [executor.submit(process, num) for num in data]
+ for future in concurrent.futures.as_completed(futures):
+ if future.exception():
+ raise Exception(f"{future.exception()}")
+ else:
+ results.append(future.result())
まとめ
繰り返し処理と並列処理の簡単な比較として、コードと実行時間の違いについて紹介しました。今回の例においては、実行時間の観点で、並列処理の方が優れていました。ただ、要件や制約によってどちらが望ましいかが変わるため、それらの整理や計測を踏まえて判断することを推奨します。