Pythonにおける並行処理の行い方
ここからはPythonのパフォーマンスチューニングに話題を移そう。最近では「Pythonが遅いのでマルチスレッドやマルチプロセスにしたい」という問い合わせが増えてきていると言う。
石本氏は「近年のパソコンはマルチCPUなので気持ちは分からなくない」と同情しつつも、「ぶっちゃけ、マルチスレッドやマルチプロセスにしただけではそう速くならない。ある程度は工夫できるが、成果はあまり期待しないこと。数倍速くなることはない。せいぜい数割。処理を速くしたければ、計算量やデータ量を削減するのが基本」と断じる。
一般的には並行処理にはthreadingモジュールやmultiprocessingモジュールを思い浮かべるかもしれないが、石本氏はPython 3.2で追加されたconcurrent.futuresのほうが「より便利なので、できるだけこれを使ってほしい」とおすすめする。
ではマルチスレッドかマルチプロセスか。石本氏は「データ処理なら、可能ならマルチスレッドで。そのほうが簡単です」と話す。マルチプロセスだとプロセス間のデータ共有や同期とか複雑、プラットフォーム依存で難しく「落とし穴にはまりがち」だそうだ。
覚えておいたほうがいいのはGIL(Global Interpreter Lock)。Pythonのインタープリターに使えるスレッドは常に1つだけ。複数のスレッドを同時に実行できない。各スレッドは5ms実行すると次のスレッドにロックを受け渡すようになっている。そのためマルチスレッドにしてもGILの制限により、Pythonの実行権限を持つスレッドは常に1つになる。
「それじゃあPythonでマルチスレッドにしても意味がないのでは」と思えるかもしれないが、石本氏は「Pythonの内部機能を使用しない処理ならGILを保持する必要がない」と言う。例えばPythonの演算にはGILが必要になるが、print()やファイルの入出力などOSやアプリケーションに関連する処理ならGILは必要ない。そのためPythonの演算とほかの処理は並行にできる。石本氏によると、それなりに性能を上げることができると言う。
実際に石本氏がNumPyで性能比較してみたところ、処理速度はスレッド数にあまり比例しないことが分かった。特に単純な演算では並行化(マルチスレッドやマルチプロセス)してもあまり効果がないとのこと。データ量が増えると4スレッド/プロセスまでは多少パフォーマンスが向上する。NumPyだとこまめにGILを開放するためマルチスレッドには効果がある。しかしメモリ転送速度が追いつかず、性能は頭打ちになってしまう。
気をつけるべきはnumpy.dot()演算だ。ライブラリ自体がマルチスレッド化しているため、ユーザーがマルチプロセスにしたりするとかえって処理が遅くなる。石本氏は「速くなるかどうかは実際にやってみないと分からないので、事前に試すこと」と話す。
GPUを使う場合にはストリームによる並行化が有効だ。ストリームとはCPUとGPUが通信するパイプのことを言う。CPUは演算命令を出すと、GPUにデータ転送されるまでは待つが(同期)、GPUの演算終了を待たずに次の演算命令を出すことができる。そのためデータ転送と演算をオーバーラップすることができる。なおNVIDIAのVisual Profilerを使うと、複数のストリームでデータ転送と演算がオーバーラップして処理されるようすが視覚的に分かる。
GPUが複数あるなら、それぞれのGPUが独立してストリームを扱えるため、データ転送を多重化することができる。石本氏は「最近だとCPUとGPUのバンド幅が広いため、同時実行が有効です。4つのGPUくらいなら飽和せずに処理できて実用的です」と話した。