Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Profilingから始めるPython高速化とRust bindings実践

Avatar for SansanTech SansanTech PRO
September 09, 2026

Profilingから始めるPython高速化とRust bindings実践

■ イベント
PyCon JP 2026 アフターイベント 〜非公式リジェクトコン〜
https://sansan.connpass.com/event/403591/

■登壇概要
タイトル:Profilingから始めるPython高速化とRust bindings実践
登壇者:技術本部 研究開発部 Architectグループ ML Architect 蔀 竜太

■ 技術本部 採用情報
https://media.sansan-engineering.com

Avatar for SansanTech

SansanTech PRO

September 09, 2026

More Decks by SansanTech

Other Decks in Technology

Transcript

  1. 使⽤するサンプルコード def match_records(source_records: list[Record], target_records: list[Record]) _> list[dict]: matched =

    [] for source in source_records: for target in target_records: match_kind = None if source.key _= target.key: # source, targetの完全一致判定 match_kind = "perfect" elif source.key in target.key: # source, targetの部分一致判定 match_kind = "source_in_target" elif target.key in source.key: # source, targetの部分一致判定 match_kind = "target_in_source" if match_kind is not None: if abs(source.amount - target.amount) <= 1: matched.append(__.) return matched # 金額の一致判定
  2. 使⽤するサンプルコード def match_records(source_records: list[Record], target_records: list[Record]) _> list[dict]: matched =

    [] for source in source_records: for target in target_records: match_kind = None if source.key _= target.key: # source, targetの完全一致判定 本番コードはより複雑なため、 # source, targetの部分一致判定 match_kind = "source_in_target" ボトルネックの特定が難しいことが多い elif target.key in source.key: # source, targetの部分一致判定 match_kind = "perfect" elif source.key in target.key: match_kind = "target_in_source" if match_kind is not None: if abs(source.amount - target.amount) <= 1: matched.append(__.) return matched # 金額の一致判定
  3. Pythonのプロファイラ - 決定的プロファイラ - cProfile: 標準ライブラリ - line_profiler: ⾏単位で測定できるプロファイラ -

    統計的プロファイラ - pyinstrument: Cythonで書かれたプロファイラ ⾮同期(async/await)コードに対応 - py-spy: Rustで書かれた低オーバーヘッドプロファイラ ネイティブコードも追跡可能 - Tachyon: Python 3.15で追加予定の標準統計的プロファイラ 低オーバーヘッドで⾮同期コードも追えるpyinstrumentを使⽤
  4. プロファイリング実施 実⾏ - サンプルコードに対して、source:3万件、target:3万件のランダムデータで検証 uv add _-dev pyinstrument # 開発時のみ必要なため開発依存として追加

    uv run pyinstrument <計測したい対象のスクリプト> 結果 - 関数ごとにかかった秒数がツリー構造で表⽰される。 43.9726 <module> main.py:1 └─ 43.9263 run_match_records_with_random_data └─ 43.8482 match_records ├─ 43.2737 [self] └─ 0.4609 abs main.py:7 main.py <built-in> main.py:78 match_records関数に時間がか かっていることがわかる。
  5. プロファイリング結果の⾒⽅ - if⽂や変数への代⼊は、[self]として表⽰される - absなどの組み込み関数は<built-in>として表⽰ - match_recordsで処理時間がかかっていることがわかる match_kind = None

    43.9726 <module> if source.key _= target.key: main.py:1 └─ 43.9263 run_match_records_… └─ 43.8482 match_records ├─ 43.2737 [self] └─ 0.4609 abs main.py:78 main.py:7 main.py <built-in> match_kind = "perfect" elif source.key in target.key: match_kind = "source_in_target" elif target.key in source.key: match_kind = "target_in_source" if match_kind is not None: if abs(source.amount - target.amount) <= 1: results.append(__.) サンプルコード(再掲)
  6. より詳細に⾒る - ⽂字列⽐較をする関数match_keyを作り、さらに分析をする - match_keyの⽂字列⽐較ではなく、match_records⾃⾝のループ処理で時間がか かっている事がわかる (時間が伸びているのは関数呼び出しが増えたため) def match_key(source_key, target_key):

    if source_key _= target_key: return 'perfect' elif source_key in target_key: return 'source_in_target' elif target_key in source_key: return 'target_in_source' return None 99.422 <module> main.py:1 └─ 99.378 run_match_… main.py:106 └─ 99.301 match_records ├─ 61.007 [self] main.py:16 main.py └─ 37.895 match_key main.py:7
  7. (余談) FastAPIのエンドポイントに対するプロファイリング middlewareとして追加して、リクエストを送信する @app.middleware("http") async def add_profiling_middleware(request, call_next): pyinstrument_profile_filename =

    f"pyinstrument_profile_{now}.html" profiler = Profiler() profiler.start() response = await call_next(request) profiler.stop() Path(pyinstrument_profile_filename).write_text(profiler.output_html()) return response
  8. ⼀番考えるべきはアルゴリズムでの⼯夫 - まず、データ構造やアルゴリズムによる改善を検討 > 事前に同じ⽂字列、⾦額をグルーピングするなど - ⽂字列⼀致判定を⾼速に⾏うことができるオープンソースライブラリ > daachorse (アルゴリズム⾃体は、Rustで実装されている)

    > pyahocorasick - ⽐較回数/ループ処理そのものを⼗分に減らせないケースでは、依然として処理時間が課題 > プロダクションコードでは、照合結果の位置情報の取得や正規化があった > FFI(Foreign Function Interface)で、PythonからRustコードを呼び出す⽅式に
  9. なぜFFIを選択したか - 変更時の要件 > 既存の資産を維持したい - APIの⼊出⼒ - ストレージからのダウンロードやアップロード -

    ボトルネック以外の既存処理の流⽤ > Pythonの拡張性を維持する - 将来的に機械学習モデルによる推論を追加 - Pythonの機械学習エコシステムとの連携 既存資産とPythonの拡張性を残しつつボトルネックとなっている処理 のみを変更の対象とするためにFFIを選択
  10. FFIの実装⼿段としてRust Bindings(PyO3)を選択 - 安全性 > Rustの型システムと所有権を活⽤ - 処理との相性 > ⽂字列⽐較‧正規化に使えるクレート(パッケージ)が豊富

    > CPUバウンドな処理を⾼速化しやすい - 実装‧運⽤ > 社内にRustの採⽤実績があり、有識者がいる > 実装‧運⽤時の相談ができる - OSSでの利⽤実績 > Polars / pydantic-core / tiktoken / orjson > Pythonライブラリで広く活⽤されている 性能だけでなく、安全性‧処理特性‧運⽤可能性‧エコシステムを含めてRust Bindings(PyO3)を選択
  11. PyO3とは - Rustの関数をPythonから呼び出せるように橋渡しするライブラリ _[pyfunction] from my_extension import sum_as_string result =

    sum_as_string(21, 21) # 42 PyO3 Python int → Rust usize Rust String → Python str fn sum_as_string(a: usize, b: usize) _> PyResult<String> { Ok((a + b).to_string()) } _[pymodule] fn my_extension(m: &Bound<'_, PyModule>) _> PyResult<()> { m.add_function(wrap_pyfunction!(sum_as_string, m)?)?; Ok(()) } PyO3 User Guide
  12. 実装の準備 maturin でRust Bindingsプロジェクト作成 uv tool install maturin maturin new

    my_extension ディレクトリ構成 . ├── main.py # Rust Bindingsの呼び出し側のコード ├── my_extension # Rust Bindingsパッケージ(Pythonから呼ぶ際のパッケージ名) │ ├── Cargo.lock │ ├── Cargo.toml │ ├── pyproject.toml │ └── src │ └── lib.rs └── pyproject.toml # Rust コード
  13. Rust実装 lib.rs :[pyfunction] fn match_records(py: Python<'_>, source_json: &str, target_json: &str)

    :> PyResult<String> { let source_records: Vec<Record> = serde_json::from_str(source_json) - #[pyfunction]で、Python側か .map_err(|e| PyValueError::new_err(format!("failed to parse source json: {e}")))?; let target_records: Vec<Record> = serde_json::from_str(target_json) ら呼ばれるように .map_err(|e| PyValueError::new_err(format!("failed to parse target json: {e}")))?; - 戻り値をPyResultにすること py.detach(:| { let mut matched = Vec::new(); で、成功値またはPython例外を for source in &source_records { for target in &target_records { 返す <キーの比較>. if match_kind.is_some() { <amountの比較> matched.push(MatchResult { source_id: &source.id, target_id: &target.id, diff: source.amount - target.amount, kind: match_kind.unwrap() }); }}} serde_json::to_string(&matched) .map_err(|e| PyValueError::new_err(format!("failed to serialize result: {e}"))) }) }
  14. パッケージ化してPythonから呼び出す Pythonパッケージとしてインストール uv add _-editable ./my_extension Python側で呼び出す def match_records_wrapper(source_records: list[dict],

    target_records: list[dict]) _> list[dict]: matched_json = my_extension.match_records( json.dumps(source_records), json.dumps(target_records), ) return json.loads(matched_json)
  15. ⼯夫した点: Rust/Python間のデータ受け渡し - PythonとRustの境界では⽂字列で受け渡しを⾏った > 引数: Python側はjson⽂字列を渡し、Rust側はそれをパースする > 戻り値: Rust側はjson⽂字列を渡し、Python側はそれをパースする

    - PythonオブジェクトをRust側で直接扱うことも可能。 > ただし、その間はGIL(Global Interpreter Lock)の影響を受ける def match_records_wrapper(source_records: list[dict], target_records: list[dict]) _> list[dict]: matched_json = my_extension.match_records( json.dumps(source_records), json.dumps(target_records), ) return json.loads(matched_json)
  16. 実務で苦労した点 - ライフタイムと所有権の設計 > レコードのコピーを避けるため、関数間の受け渡しとマッチ結果を参照で保持 > 関数終了後も参照が有効になるようライフタイムの設計が必要 > コンパイルエラー発⽣時は、⽣成AIも活⽤して複数の設計案を⽐較し、修正⽅針を整理 -

    Rustでの⽂字列の扱い > それぞれの関数ごとに⽂字単位で出⼒されるか、バイト単位で出⼒されるのかを⾒極め る必要があった fn print_length() { let text = "請求書"; println!("bytes: {}", text.len()); _/ bytes: 9 println!("chars: {}", text.chars().count()); _/ chars: 3 }
  17. Rust Bindings書き換え後のプロファイリング - 43.9秒 → 6.6秒: 約6.7倍⾼速化 - 実際のプロダクションコードでは、アルゴリズムの改善も組み合わせて約30~44倍⾼速化を実現 6.7287

    <module> 43.9726 <module> ├─ 6.6501 run_match_… main.py:1 main_rs.py:70 │ ├─ 6.5698 match_records_wrapper │ │ ├─ 5.8504 match_records │ │ └─ 0.7085 loads main.py │ │ <built-in> │ └─ 0.0792 generate_random_records └─ 43.9263 run_match_… main.py:78 └─ 43.8482 match_records ├─ 43.2737 [self] └─ 0.4609 abs main_rs.py:1 main.py:7 <built-in> json/__init__.py:304 [2 frames hidden] └─ 0.0774 [self] main_rs.py:11 json main_rs.py:24 main_rs.py Python実装(再掲) Rust Bindingsでの実装
  18. まとめ - パフォーマンス改善では、まずプロファイリングで現状を分析する - アルゴリズムの改善だけでは解決しないときの⼿段として、⾔語レベルの⾼速化も検討 > 今回はボトルネック部分だけをRust化し、約6.7倍⾼速化 - Rust Bindingsはすべてのケースで最適とは限らないが、既存のPython資産を活かしながら

    ⼀部を⾼速化する有効な選択肢 > 既存資産、チームの技術スタックなどを含めて選定 - 現状の運⽤では、Rust未経験者も機能追加を担当し、運⽤⼈数を増やす取り組みを実施 > ⽣成AIの⽀援もあり、Python実装時と⽐べて開発時間は⼤きく増えていない