はじめに
pythonでいろいろプログラムを実行していると、もっと高速に実行することはできないかなと思うことがあります。
特に性能のいいパソコンなどを持っている人だと、タスクマネージャーを開いてCPUの使用率を眺めていると1スレッドしか使われていないじゃん、と思うことがあると思います。
何とかしてすべてのcpuコアを使用して高速化できないかな、というときに少し調べたらこんな記事が出てきました。
私もこれを参考にいろいろ高速化に成功しましたが、いろいろ落とし穴などがあったため、並列化のやり方を1から解説しようと思います。
どんなコードなら高速化できるか
なんでもかんでも高速化することができるわけではありません。
大前提として、___forを使用したループ___がある場合、並列化によって高速化ができる可能性があります。
可能性といったのは、中には難しい場合もあるからで後に解説します。
実際どうやるの?
例えば以下のようなプログラムを高速化しようとします。
for i in range(30):
print(i)
print("の2乗は")
print(i*i)
print()
これを高速化する場合、二つのステップがあります。
1.ループ内の関数化
マルチスレッドにするのに伴い、ループ毎に行っている内容を関数として書き直します。
つまり今回は、このように書き直します。
def square(i):
print(i)
print("の2乗は")
print(i*i)
print()
for i in range(30):
square(i)
2.Poolの導入
次に、並列化を実装します。
並列化では、multiprocessingというpython標準ライブラリからPoolという関数を導入します。
from multiprocessing import Pool
次に、実行方法を変えます。
if __name__ == '__main__':
main()
こんなプログラムを見たことはないでしょうか。
最初見たときなんだこれ、と思いましたが普通に使う分には、if __name__ == '__main__':
の中に実行したいプログラムを書くことでいつものように実行することができます。
よくある「おまじない」と解説されるような内容ですが、詳しく知りたい人はこんな記事を読んでください。
とにかく今回重要なのは、このif __name__ == '__main__':
を使うということです。
コードは、以下のようになります。
わかりやすいように最初のコードも載せておきますね
for i in range(30):
print(i)
print("の2乗は")
print(i*i)
print()
from multiprocessing import Pool
def square(i):
print(i)
print("の2乗は")
print(i*i)
print()
if __name__ == '__main__':
p = Pool()
p.map(square, range(30))
まず、
p = Pool()
の部分ですが、Pool(6)
などと指定すると、6スレッド使用するように指定することができます。何も書かないとそのパソコンのCPUの最大数を使ってくれるみたいです。
そして一番大切な部分が、これです。
p.map(square, range(30))
map関数の第一変数に実行したい関数を、第二変数に関数に代入したいものを指定します。
第二変数はリストだったりrangeだったり、forでのループ時に使用していたものそのままでいいです。
a = [1,3,5,7]
p.map(square, a)
などという書き方もできます。
ここで注意ですが、この並列化では先頭から順番に代入してくれません。詳しくはまた後程説明します。
注意
1.実行方法の変更
これは普段エディタ上でプログラムを動かしている人向けなのですが、基本的にエディタ上では並列化のプログラムは動きません。(たぶん)
私は普段anacondaについてきたspyderを使用しているのですが、そこでは動きません。
実行時はコマンドプロンプトから実行してください。
いちいちコマンドを入力するのが面倒臭いという人はこの記事を参考にバッチファイルを作るのがおすすめです。(Windows)
2.ループ内の実行順番
並列化では順番無視でどんどん実行していきます。
たとえば、以下のような数字を順番に表示させるプログラムがあるとすると、
from multiprocessing import Pool
if __name__ == '__main__':
p = Pool()
p.map(print, range(3000))
こんな風に最初はいいのですが、途中から順番が狂ってきます。
0
1
2
3
:
:
23
24
25
26
27
47
28
48
94
:
:
2819
2866
2912
2956
2913
2957
2958
2959
2960
なので並列化させたい場合は、__実行順番に意味がない__プログラムを使用してください。
3.変数の数
この並列化では、並列化する関数に渡せる変数は一つのみです。
この一つというのはこういうことです。
from multiprocessing import Pool
def plus(a,b):
print(a+b)
if __name__ == '__main__':
a = [1,3,5,7,9]
b = [2,4,6,8,10]
p = Pool()
p.map(plus2, a, b) #ここでエラー mapはaとbを同時に渡せない
関数に渡せるオブジェクトが1つということで、リストにして複数渡すということができます。
この場合の並列化のプログラムはこんな感じです。
from multiprocessing import Pool
def plus2(c):
print(c[0] + c[1])
if __name__ == '__main__':
c = [[2,5],[4,7],[8,9],[3,4],[5,11],[4,7],[2,5],[8,6]]
p = Pool()
p.map(plus2, c)
複数受け取る関数を書き直したくないという方は、これを参考にしてください。
2021/6/11追記
@relu さんからstarmap
を使用すれば関数を書き直さずに複数変数を渡せるという情報をいただきました。
from multiprocessing import Pool
def plus(a,b):
print(a+b)
if __name__ == '__main__':
c = [[2,5],[4,7],[8,9],[3,4],[5,11],[4,7],[2,5],[8,6]]
p = Pool()
p.starmap(plus, c)
もしくは
from multiprocessing import Pool
def plus(a,b):
print(a+b)
if __name__ == '__main__':
a = [1,3,5,7,9,11]
b = [2,4,6,8,10,12]
p = Pool()
p.starmap(plus, zip(a,b))
このようmap
をstarmap
にし、渡したい変数をリストにしたものをリストにして渡すか、変数ごとに順番に対応のあるリストにしてzip
関数を使って渡すことで複数変数受け取る関数のまま使用することができます。
並列化できないプログラム
ここまで見ると、forループさえあればどんなプログラムでも並列化できるように思えます。
しかし、forループでなく並列化することによってできなくなることがあります。
その多くがfor特有のものだったりします。
以下は私が並列化を使用していて実際にできなかった例です。
これらのほかにも並列化できない内容があると思います。
1.breakとcontinue
forを使っている中で、条件を満たしたらループ終了や、実行しないようにするためにbreak
や continue
を使用していることがあると思います。
しかし並列化するとループという概念ではなくなるので、以上のものが使われているとエラーが起きます。
ただし、並列化された関数の中にさらにforループがあり、その中のbreak
や continue
は関係ありません。
2.tryとexcept
for内で、エラーが生じたときの処理としてtry
とexcept
を書くことがありますが、それがあっても動きません。
try
とexcept
などはfor
ループ特有の機能ではなく、いつでも
使えるので、関数内にあっても問題ありません。
3.共通変数(2021/6/13追記)
並列化した関数からglobal変数にアクセスしようとするとエラーになります。
というのも、並列化しているため、共通のメモリにアクセスできないからだそうです。
やはり並列化したい関数が完全に独立していないと並列化は難しいですね
from multiprocessing import Pool
def plus(a,b):
print(a+b)
global result
result.append(a+b)
if __name__ == '__main__':
result = []
a = [1,3,5,7,9,11]
b = [2,4,6,8,10,12]
p = Pool()
p.starmap(plus, zip(a,b)) #動かない
まとめ
並列化を使用していて詰まったことを中心に、深く理解しなくても高速化する方法を書きました。
特にopencvで画像を処理する際などに使用しているのですが、使用すると非常に高速化ができます。
ぜひ使用してみてください。
参考にしたサイト