[2023] Cómo hacer Python 100 veces más rápido con PyO3
(ohadravid.github.io)Últimamente, mientras estudiaba Python con free-threading, me interesó PyO3, así que comparto este artículo aunque ya tiene 2 años.
Making Python 100× Faster with <100 Lines of Rust – resumen
Contexto
- La librería principal de Python de un pipeline interno de procesamiento 3-D se convirtió en un cuello de botella al aumentar los usuarios concurrentes.
- Reescribir todo en Rust implicaba demasiado riesgo y tiempo, así que se eligió una optimización parcial.
Enfoque
- Primero medir: identificar el cuello de botella con el profiler de muestreo
py-spy. - Introducción gradual de Rust
- Conectar Python ↔ Rust con
PyO3+maturin. - Primero, portar solo la función
find_close_polygonsa Rust. - Después, mover también la estructura de datos
Polygona Rust y hacer subclassing desde Python.
- Conectar Python ↔ Rust con
- Perfilado y mejora iterativos
- Minimizar conversiones innecesarias de NumPy → Rust.
- Reducir asignaciones y copias, y aplicar microoptimizaciones con cálculo directo de distancias.
Cambios de rendimiento
| Etapa | Tiempo promedio de ejecución (ms) | Multiplicador de mejora |
|---|---|---|
| Python puro inicial | 293.41 | 1× |
v1 – solo la función en Rust (--release) |
23.44 | 12.5× |
v2 – Polygon también en Rust |
6.29 | 46.5× |
| v3 – eliminación de asignaciones y cálculo directo | 2.90 | 101× |
Tecnologías clave
- PyO3 : FFI segura entre Python ↔ Rust.
- maturin : automatización de build y distribución.
- ndarray / numpy crate : arreglos y álgebra lineal del lado de Rust.
- py-spy : profiler que permite ver incluso el stack nativo.
Lecciones
- Si primero haces profiling, puedes obtener grandes ganancias con cambios pequeños en el código.
- Incluso manteniendo la API de Python, reemplazar solo el módulo en Rust permite aplicarlo de inmediato a un servicio en producción.
- Rust es suficientemente efectivo incluso si se introduce de forma acotada solo en las “zonas de rendimiento”.
3 comentarios
Crear extensiones de Python con C/C++ reduce demasiado la productividad, pero PyO3 es muy cómodo porque de entrada cuenta con
maturinycargo.Además, en los módulos de Python la compilación cruzada también es indispensable, y con Rust incluso eso es sencillo.
maturin... dolor...
Aguanta lo más posible con la vectorización de NumPy; si no alcanza, mete una GPU y pásate a CuPy o Torch, y si aun así no basta, escribe código nativo con Cython o algo por el estilo... pero parece que, en lo posible, es mejor evitar lo nativo. Es pesado.