- El proyecto CPython introdujo recientemente una nueva estrategia de implementación para el intérprete de bytecode. Los resultados iniciales mostraron una mejora promedio de rendimiento de 10-15% en varias plataformas
- Sin embargo, esta mejora de rendimiento fue principalmente el resultado de evitar un problema de regresión en LLVM 19. Al compararlo con referencias mejores (por ejemplo, GCC, clang-18, LLVM 19 con ciertas banderas de ajuste), la mejora de rendimiento se reduce a 1-5%
Resultados de rendimiento
- Se hicieron benchmarks de varias compilaciones del intérprete de CPython usando distintos compiladores y opciones de configuración. Se probó en un servidor Intel y en una Macbook Air con Apple M1.
- Todas las compilaciones usaron LTO y PGO. Se usó como referencia
clang18 y el promedio reportado en pypeformance/pyperf compare_to.
- Comparación de rendimiento entre compiladores
- Macbook Air con Apple M1 :
- clang18: referencia
- clang19: 1.12 veces más lento
- clang19.taildup: 1.02 veces más lento
- clang19.tc: 1.00 veces más lento
- gcc: N/A
- El intérprete con llamada en cola seguía mostrando una mejora de velocidad frente a clang-18, pero la degradación de rendimiento al pasar a clang-19 fue más drástica.
Regresión en LLVM
Contexto breve
- Un intérprete tradicional de bytecode está compuesto por una sentencia
switch dentro de un bucle while. La mayoría de los compiladores compilan switch como una tabla de saltos.
- Los compiladores modernos de C soportan el patrón de tomar la dirección de labels y usarla como un "goto calculado". CPython usó este patrón hasta el trabajo de llamada en cola.
Regresión en LLVM 19
- LLVM 19 impuso límites al paso de tail-duplication para que deje de duplicar cuando el tamaño del IR supera cierto umbral. Como resultado, en CPython todos los saltos de despacho se fusionaron, anulando por completo el propósito de la implementación basada en
goto calculado.
Otras anomalías
- Hay confianza en que el cambio en la lógica de duplicación para llamadas en cola causó la regresión, pero no se puede explicar completamente la magnitud de esa regresión.
- En procesadores modernos, una mejora de velocidad de 2-4% es más común.
¿Es necesario el goto calculado?
- El benchmark
clang19.nocg afirma ser más rápido que clang19. Esto muestra que el compilador puede aplicar la misma optimización usando un intérprete basado en switch.
Corrección
- El pull request 114990 de LLVM corrigió la regresión. Esta corrección restaura el rendimiento esperado.
Reflexiones
Sobre los benchmarks
- Al optimizar un sistema, se construyen benchmarks y una metodología de benchmarking, y se evalúan los cambios propuestos.
- Los benchmarks requieren más supuestos y creencias para generalizar puntos de datos específicos.
Línea base
- Al proponer una solución o método nuevo, es común compararlo con "el mejor enfoque conocido actualmente".
Sobre la ingeniería de software
- Los sistemas de software son complejos, están interconectados y cambian rápidamente.
- Los compiladores optimizadores viven en una tensión entre respetar la intención del programador y optimizar el código.
Compiladores optimizadores
- El atributo
musttail representa un nuevo tipo de funcionalidad de compilador relacionada con la optimización. Puede ofrecer un estilo más potente para escribir código sensible al rendimiento.
Una cosa más sobre nix
nix fue muy útil en este proyecto. Ayudó mucho a administrar y compilar múltiples versiones del intérprete de Python.
1 comentarios
Comentarios de Hacker News
Hola. Soy el autor del PR que introdujo el intérprete con tail-calling en CPython
Hacer benchmarking realmente es una tarea muy difícil
Aplausos al autor por llegar al fondo de la verdad de este problema
Este es un buen ejemplo de que C no es un lenguaje "cercano a la máquina"
Al ajustar la forma en que el compilador organiza el bucle, el intérprete con tail-call no resulta tan efectivo como se anunció
Evaluar el rendimiento de los builds de Python es muy difícil
Discusión relacionada:
Excelente artículo
Recientemente hice benchmarking desde Python 3.9 hasta 3.13
Me pregunto cómo se relaciona este tipo de optimización con la optimización de tail-call