pythonビギナーがパフォーマンス改善のためにやった6つの約束事

petty pet (a python)!
Photo by Rajarshi MITRA

Ethereumが気になっているフロントエンドエンジニアの渡邊ことタイショウです。みなさまいかがお過ごしでしょうか。

タイトル通りpythonの記事です。pythonです。pyjsとかではないです。最近フロントエンドエンジニアという肩書きが形骸化しはじめています。某エンジニアの方からは「フロントエンド=最前線では・・・」と言われてますが、多分鉄砲玉の意味合いだと思います。

某案件にていままで使ったことのないpythonを採用しました。採用した理由は単純明快ではありますが、超大量のデータ+多次元配列での演算を実現する必要があったのでどう考えてもnumpyが最適解になりました。

ただやはり問題はあるもので、今回要求されているのは高速のレスポンスでして、そのまま素直に実装を行なっても要求される水準でのレスポンスは望めませんでした。

色々試行錯誤の末、改善の効果を見込めた(python使い慣れている人なら知ってて当然思われる)6つのお約束事を実施しました。処理速度は素直に(何も考えずに)やった時の大体100分の1程度になりました。一部は@toritsuyo大先生の入れ知恵です。足を向けてねれません。

[pythonビギナーがパフォーマンス改善のためにやった6つの約束事]
1.cython  
2.リスト内包表記  
3.joblib  
4.rq(Redis Queue)  
5.messagepack  
6.sqlクエリの見直し  

以降の内容はpythonを実務でよく使われていらっしゃる方には見慣れたものだけなので、ここで読み終えていただくか、是非HAROiDへJOINいただきまして、まだ最適化されていない拙者のコードに対して罵詈雑言を与えていただければ幸いです。

HAROiD - Wantedly

1. cython

np.ndarrayを扱う部分やnumpy,scipyの演算部分がかなりボトルネックになっていたので、どうしたもんかなあと色々やっていたところ、cythonが早くなるよ!というのをちらほら見かけたのでノータイムで導入しました。

が、cythonは型指定しないと最適化されないので、numpyをそのまま扱ってもあまりいい結果を得ることができませんでした。

色々調べてみるとndarrayをcythonの型として取れるようで、

cimport numpy as np  

でndarrayを型として認識してくれた結果、実行時間は25分の1まで短縮できました。

2. リスト内包表記

python慣れている方にとっては今更感満載だとは思いますが。

filter(lambda item:item=="hoge",items)  

よりも

[item for item in items if val=="hoge"]

の方が早いという話です。以上。(なんで上の記法あるんでしょうね。謎。)

3. joblib

c_items = []  
for a_item in a_items:  
    tmp_items = [b_item for b_item in b_items if a_item>b_item]
    c_items.extend(tmp_items)

かなり多いデータ量のリストを扱って、ネストするループ処理に入れる場合は並列実行した方がはやい場合が往々にしてありえます(マシンスペックにもよりますが)。

def match_a(a_item,b_items):  
    return [b_item for b_item in b_items if a_item>b_item]

tmp_items = Parallel(n_jobs=-1)([delayed(match_a)(a_item,b_items) for a_item in a_items])  
c_item = []  
[c_items.extend(tmp_item) for tmp_item in tmp_items]

並列処理はtopで眺めてると実際たのしい。

4. rq(Redis Queue)

大先生による入れ知恵です。1プロセスの中で1.非常に重い処理を持っていて 2.そのプロセス中に結果を返す必要がない部分(主にループで重い処理やってる場合)。要は非同期でオッケーな部分を切り離したい場合に非常に有効。joblibは2がFalseなケースなのでこの用途では使えないので使い分けます。

rqを採用するメリットはworkerがメインプロセスと同一のサーバーにある必要がないので実行環境を切り離すことができるのが大きいかなと(同じ環境でももちろん可能)。Worker Threadパターンだそうです。っょぃょ

from redis import Redis  
from rq import Queue

conn = Redis(host, 6379)  
q = Queue(connection=conn)  
for item in items:  
    q.enqueue(othermodule.method, item)

topで見てると楽しい(2回目)

5. messagepack

数M〜数十Mbyteのデータをほぼ連続的に別の実行環境に送り続けるような処理を行なっているので、できる限りデータサイズは抑えたいなあと思った際に検討していた中にありました。jsonで文字列で渡す場合と比較した場合に半分以下になった上に圧縮・解凍の処理性能もかなり良い結果だったので採用しました。ただ何人かバージョンの互換性に苦しめられたとか聞いたのでその辺は注意が必要そうではありました。

import msgpack

data = {"list":["hugahuga",.....,"higehige"]}  
comp = msgpack.packb(data)  

6. sqlクエリの見直し

n+1とかやらかしていたのでほんと反省すべき。そうすべき。レビューさせるのも恥ずかしいのでちゃんと直してからリクエストだしましょう(戒め)

まとめ

色々やってみた中で一番効果が大きかったのは cython ですが、静的型付けでの実装を強いられるので、外部のモジュールを使用している場合などだと使用すること自体が難しいケースもあるかと思います。基本的には自分で実装している部分やnumpyで実装している部分で使うのがベターかなと思います。ご利用は計画的に。

並列処理 はループのネストがある場合なんかは、データ量が極端に少なくない限りはとりあえず試してみるといいのではないかなと。

messagepack はpublicなapiを提供しなければならないケースだと忌避される可能性あるかと思いますが、裏側でガシガシやりたい場合や各実行環境のコントロールができる場合なんかは採用の余地はありかなーと。

他のポイントはチリツモ案件だと思います。着手の段階からちゃんと気をつけましょう。

以上。これまでやってきた改善内容をせきららに披瀝いたしました。現在進行形で改善はやっている最中なのでこれええやんとかいうのがあればまた。