Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
124
Help us understand the problem. What is going on with this article?
@yuki_2020

細かいことは理解せずにpythonのコードをマルチスレッドで高速化する方法

はじめに

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__':を使うということです。

コードは、以下のようになります。
わかりやすいように最初のコードも載せておきますね

最初のコード.py
for i in range(30):
   print(i)
   print("の2乗は")
   print(i*i)
   print()
最終的なコード.py
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.変数の数

この並列化では、並列化する関数に渡せる変数は一つのみです。
この一つというのはこういうことです。

ダメな例.py
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つということで、リストにして複数渡すということができます。

この場合の並列化のプログラムはこんな感じです。

いい例.py
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))

このようmapstarmapにし、渡したい変数をリストにしたものをリストにして渡すか、変数ごとに順番に対応のあるリストにしてzip関数を使って渡すことで複数変数受け取る関数のまま使用することができます。

並列化できないプログラム

ここまで見ると、forループさえあればどんなプログラムでも並列化できるように思えます。
しかし、forループでなく並列化することによってできなくなることがあります。
その多くがfor特有のものだったりします。
以下は私が並列化を使用していて実際にできなかった例です。
これらのほかにも並列化できない内容があると思います。

1.breakとcontinue

forを使っている中で、条件を満たしたらループ終了や、実行しないようにするためにbreakcontinueを使用していることがあると思います。
しかし並列化するとループという概念ではなくなるので、以上のものが使われているとエラーが起きます。
ただし、並列化された関数の中にさらにforループがあり、その中のbreakcontinueは関係ありません。

2.tryとexcept

for内で、エラーが生じたときの処理としてtryexceptを書くことがありますが、それがあっても動きません。

3.共通変数(2021/6/13追記)

並列化した関数からglobal変数にアクセスしようとするとエラーになります。
というのも、並列化しているため、共通のメモリにアクセスできないからだそうです。
やはり並列化したい関数が完全に独立していないと並列化は難しいですね

実行できない例.py
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で画像を処理する際などに使用しているのですが、使用すると非常に高速化ができます。
ぜひ使用してみてください。

参考にしたサイト

124
Help us understand the problem. What is going on with this article?
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
yuki_2020
独学でpythonを勉強中

Comments

No comments
Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account Login
124
Help us understand the problem. What is going on with this article?