概要
Pythonでkago-utilsという麻雀ゲームを作っている。
こいつの処理がもっさりしていたので、向聴数計算の処理をCで書き換えることによって速度を改善した。
背景
kago-utilsのゲームルーチンにバグがないかを検証するために、天鳳の牌譜を約60万半荘分流し込むという作業をしていた。
しかし、1半荘を終えるのに1秒程度かかっていたので、検証作業に非常に時間がかかっていた。
そこでyappiというツールを使って牌譜10半荘分のデータを流した時の各関数の実行時間を計測したところ、以下のようになった。
HaihuParser.run
が一番外側の呼び出し関数で、これが350秒かかっている。
どうやらyappiの差し込みの処理が重いらしく、実際は10秒程度で終わっている処理に350秒程度かかってしまっているようだが、各関数の処理時間の比率自体はそこまで変わらないと想定して、どの関数がボトルネックになっているかを考えることにした。
この画像を見て分かる通り、向聴数の計算を行うcalculate_shanten
に338秒かかっていて、これは全体の処理時間の96%程度を占めていることが分かる。
そこで、向聴数計算の処理だけをC拡張で実装して速度改善を図ることにした。
全部の実装をCで書き換えることができればベストなんだろうが、自分はCが書けないし、既にほぼ全てのコードを書き終えてしまっているので、今回は向聴数計算の部分だけをCで実装することで手打ちにした。
方法
正直、方法については調べればいくらでも情報は出るので、コードと細かいTipsだけを共有する。
Tips1. C vs C拡張
Cで実装するのにも2通りの方法がある。Cを直接呼び出す方法と、C拡張モジュールを使う方法だ。
Cを直接呼び出す方法は、ビルドしたCのコードをPythonから直接呼び出す方法だ。
そのため、PythonとCの間でデータをやり取りするためのコードを書く必要がある。
長所としては、PythonのコードとCのコードが疎結合になりやすいこと、特別な設定を必要としないことなどが挙げられる。
短所としては、PythonのデータをCのデータに変換する過程でオーバーヘッドが発生することなどが挙げられる。
C拡張モジュールを使う方法は、PythonのC APIを使ってCのコードをPythonのモジュールとしてビルドする方法だ。
長所としては、PythonのデータをほぼそのままCに渡せるので速度が早いことなどが挙げられる。
短所としては、CのコードとPythonのコードと密結合になること、Pythonから直接データを受け取るための専用の関数を覚える必要があること、Cのコードをビルドするためにsetup.py
の設定が必要になることなどが挙げられる。
今回はあまりメンテする必要がないこと、速度改善が最重要の目的であることなどからC拡張モジュールを使う方法を選択した。
Tips2. Python.h
の場所
C拡張モジュールを使う場合、Python.h
というヘッダーファイルをインクルードする必要がある。
ただビルドするだけならPythonが勝手にPython.h
の場所を探してよしなにやってくれるのだが、もしエディタで補完の恩恵を受けたいならPython.h
の場所をエディタに教えてあげる必要がある。
C拡張モジュールは専用の関数が多いので、補完が効かないと非常に不便だ。
Python.h
のパスはターミナルでpython3 -c "from sysconfig import get_paths; print(get_paths()['include'])"
のように実行すると取得できる。
VSCodeなら設定画面でC_Cpp.default.includePath
に上記のパスを追加してやれば補完が効くようになる。ただしC/C++
という拡張機能を入れておく必要がある。
Tips3. setup.py
の追加
拡張モジュールを使う場合、setup.py
というファイルを追加して、Cのコードをビルドするための設定を記述する必要がある。
自分の場合はsetup.pyのように現状ではシンプルな内容になっているが、色々なリポジトリを見て回った感じ、いくらでも複雑な構成にできそうだった。
怖いのであまり奥深くには立ち入らないことにする。
Tips4. .pyi
を追加する
.pyi
を作成することでPythonのコードからC拡張モジュールのコードを呼び出すための型ヒントを追加することができる。
これを行うことでエディタのインテリセンスが効くようになので、やっておいて損はないだろう。
Tips5. 【告白】 CのコードはほぼChatGPTに書かせた!!!!
自分はCが書けないので既存のPythonのコードをChatGPTに投げて、「C拡張に変換して」とお願いしたらほぼ完成形のCのコードを書いてくれた。
一部だけ修正して終了。
いい時代になった…。
結果
再度10半荘分の牌譜を流し込んで関数の処理時間を計測したところ以下のようになった。
向聴数計算が処理時間上位にランクインすることはなくなり、全体の処理時間も約13秒程度にまで短縮された。
またpytest-benchmarkの結果をGitHub ActionsでGitHub Pagesに自動デプロイの記事で解説したベンチマーク計測処理を向聴数の計算にも適用していたのだが、その結果は以下のようになった。
6cee0a6
がC拡張を適用したコミットである。
画像から向聴数の計算だけを切り取っても50倍程度の速度改善が見られる。
まとめ
60万半荘分の牌譜を使った検証が1日で完了するようになった。
満足。
コメント