クラスメソッドをjoblibしたらかえって遅くなった話
TL;DR
joblibでマルチコア並列したろ!と思って雑に投げたらかえって遅くなりました.
class methodを並列化したんですが,classのメンバ変数が空間計算量的に大きい場合にこの現象がおきると考えられます.
selfにインスタンスが参照ではなく値で渡されている?ためかなと思っていますが,詳細には未解明です.
classメソッドをclass外に切り離して,関数に渡すデータ量を絞ったらちゃんと早くなりました.
なにが思ったより早くならなかったか.
class myClass:
def __init__(self):
self.big_data
def func(self,x):
hogehoge
メンバにそこそこ大きな(数GB)self.big_dataという変数を持つクラスを使っていました.
そして,funcはbig_dataを参照して,そのデータに基づきxを加工して出力する関数で,結構な数のxをさばく必要があったので,joblibで雑に並列化することを考えました.
C = myClass()
ret = Parallel(n_jobs=-1)[delayed(C.func)(x) for x in x_list]
これがめちゃくちゃ逆効果で遅くなりました.
なんで早くならなかった?
これは自明で,オーバーヘッドが大きすぎたからです.
オーバヘッドが原因なのはすぐに読めたので,activity monitorでプロセスを眺めたところ,Pythonがメチャクチャなメモリを使っていることが判明しました.
こんなにメモリを食うのは,
- マルチコア並列化のときにそもそもインスタンスがコピーされてる
- selfの受け渡しの度にインスタンスのコピーが渡されてる
のどっちかだと思ったので,それを解決することにしました.
どう解決した?
クラスメソッドを外に切り離して,selfが渡されないようにしました.
具体的には今回の関数funcはself.big_dataを参照するのですが,参照するべき場所はすぐに求まるので,入力xに大してbig_dataのどこを参照するかを返す関数x2indexを定義した上で,
def func(x,part_of_big_data):
hogehoge
def x2index(x):
return some_index
class myClass:
def __init__(self):
self.big_data
C = myClass()
ret = Parallel(n_jobs=-1)[delayed(func)(x,C.big_data[x2index(x)]) for x in x_list]
とすることで対処しました.
こうするとちゃんと早くなりました.(ただし並列化効率はあまり上がらず…)
まとめ
joblibで並列化するときはクラスメソッド渡さないほうが多分安牌です.
(データ数が小さくても毎回selfが渡される分無駄になると思われます.)
わかってないこと
selfが参照じゃなくてコピーされる?のはそももそもpython自体の仕様なのか,マルチコア並列するときに生じる問題なのかが良くわかっていません.
このあたりはOSとハードウェアあたりをもう少し勉強したいと思っています.
Discussion