[2023] Rendre Python 100× plus rapide avec PyO3
(ohadravid.github.io)Je m’intéresse à PyO3 en étudiant récemment le free-threading de Python, donc je partage cet article qui date de deux ans.
Making Python 100× Faster with <100 Lines of Rust – résumé
Contexte
- La bibliothèque Python centrale d’un pipeline interne de traitement 3-D créait un goulot d’étranglement avec l’augmentation du nombre d’utilisateurs simultanés.
- Réécrire l’ensemble en Rust était trop risqué et trop long, donc le choix s’est porté sur une optimisation partielle.
Méthode
- Mesurer d’abord : identification du goulot d’étranglement avec le profiler d’échantillonnage
py-spy. - Introduction progressive de Rust
- Connexion Python ↔ Rust avec
PyO3+maturin. - Portage d’abord de la seule fonction
find_close_polygonsen Rust. - Puis déplacement de la structure de données
Polygonen Rust, avec sous-classage depuis Python.
- Connexion Python ↔ Rust avec
- Boucle profilage-amélioration
- Réduction au minimum des conversions inutiles de NumPy → Rust.
- Diminution des allocations et des copies, avec micro-optimisations via calcul direct des distances.
Évolution des performances
| Étape | Temps d’exécution moyen (ms) | Facteur d’amélioration |
|---|---|---|
| Python pur initial | 293.41 | 1× |
v1 – fonction seule en Rust (--release) |
23.44 | 12.5× |
v2 – Polygon aussi en Rust |
6.29 | 46.5× |
| v3 – suppression des allocations, calcul direct | 2.90 | 101× |
Technologies clés
- PyO3 : FFI sûre entre Python et Rust.
- maturin : automatisation du build et du déploiement.
- ndarray / numpy crate : tableaux et algèbre linéaire côté Rust.
- py-spy : profiler capable d’afficher aussi la pile native.
Enseignements
- En profilant d’abord, on peut obtenir de gros gains avec de petites modifications de code.
- Même en conservant l’API Python, remplacer uniquement le module Rust permet une application immédiate en production.
- Rust est déjà très efficace même lorsqu’il n’est introduit que de façon ciblée sur la zone de performance.
3 commentaires
Créer des extensions Python en C/C++ fait trop chuter la productivité, alors que PyO3 est vraiment pratique grâce à
maturinetcargo.De plus, la compilation croisée est indispensable pour les modules Python, et Rust la rend également simple.
maturin... quelle galère...
On tient au maximum avec la vectorisation NumPy, et si ça ne suffit pas, on branche un GPU et on passe à CuPy ou Torch ; si ça ne suffit toujours pas, on écrit du natif avec Cython… mais à mon avis, mieux vaut éviter le natif autant que possible. C’est difficile.