この記事は 「Pythonって、たぶんそういう意味じゃない」シリーズ の第11弾です。
Python歴5年以上、メインで使い続けてきた変態が書いています。
この記事は 個人の経験に基づくポエムです。
環境・用途・経験によって異なる場合があります。
理由はこだわりだ。
自分が作っているOSS、ValidKitのバリデーション速度が気になった。
遅いわけじゃない。でも速くできるなら速くしたい。それだけだった。
ベンチマークを取った
Running benchmark with 50000 iterations...
Standard validate: 0.3490 seconds
Compiled validate: 0.1033 seconds
Speedup: 3.38x faster!
5万回のバリデーションで、0.3490秒が0.1033秒になった。
3.38倍。
こだわりの結果としては、悪くない数字だと思っている。
何をしたのか
ValidKitは辞書ベースのスキーマでバリデーションを行うライブラリだ。
SCHEMA = {
"ユーザー名": v.str().regex(r"^\w{3,15}$"),
"レベル": v.int().range(1, 100),
}
通常のバリデーションは、このスキーマを毎回解釈しながらチェックする。
辞書を辿って、型を確認して、条件を評価して……という処理を、バリデーションのたびに繰り返す。
これはeval()の記事で書いた話とも近い。
「毎回解釈する」処理は、どうしてもオーバーヘッドが乗る。
コンパイル機能でやったこと
そこで、事前コンパイルという仕組みを追加した。
スキーマを毎回解釈する代わりに、
そのスキーマ専用の、最適化されたPythonコードを動的に生成する。
生成したコードはexec()でコンパイルし、以降はそのコードを直接実行する。
compiled = compile_schema(SCHEMA)
# 以降はコンパイル済みの専用関数が呼ばれる
compiled.validate(data)
exec()を使っている。
ここもeval()の記事と同じ話で、動的にコードを実行する以上、生成するコードの安全性には気を配る必要がある。
ValidKit内部では、ユーザー入力を直接exec()に渡すことはなく、スキーマ定義から生成した固定パターンのコードのみを実行している。
なぜ速くなるのか
普段のバリデーションは、スキーマという「地図」を見ながら毎回道を探すようなものだ。
コンパイルは、その地図をもとに一度だけ最短ルートを計算して、以降はそのルートを直接進むイメージに近い。
- 毎回の型チェックの分岐がなくなる
- 辞書アクセスの回数が減る
- そのスキーマでしか通らない処理だけが残る
汎用的な処理を、そのスキーマに特化した専用処理に置き換えることで速度が出る。
こだわった結果、どうなったか
正直、3.38倍にしたところで、体感できる速度差がある場面は限られている。
大量のデータを一括でバリデーションする用途なら効果は大きいが、
Discordボットの設定ファイルを1回読むだけなら、正直誤差の範囲だ。
それでも、速くできるなら速くしたい。
実用性より、こだわりが先に来ることがある。
今回はそれだった。
まとめ
- 通常のバリデーション → スキーマを毎回解釈する
- コンパイル機能 → スキーマ専用のコードを動的生成して
exec()で実行 - 結果 → 3.38倍速くなった
- 理由 → こだわり
ValidKitはPyPIで公開している。気になったら触ってみてほしい。
「Pythonって、たぶんそういう意味じゃない」 シリーズでは、
こういう"なんとなくで語られがちなPython"を言語化していきます。
👉 ストックをフォローしておくと次の記事を見逃しません!