El compilador JIT de Python por fin está aquí
Python 3.15 llega con un compilador JIT copy-and-patch en el que se lleva años trabajando. Cómo funciona, qué mejoras de velocidad esperar y por qué CPython tardó tanto.

Python ha sido «demasiado lento» desde que existe. La respuesta habitual de la comunidad de Python, «usa extensiones en C para los bucles críticos», siempre ha sido una concesión: el modelo de ejecución por defecto del lenguaje tiene limitaciones de fondo. CPython interpreta el bytecode una instrucción a la vez, y cada instrucción se despacha mediante una sentencia switch. Es simple, portable y fácil de depurar. También es unas 100 veces más lento que C compilado en tareas con mucho cómputo.
Python 3.15 cambia esto. Tras años de trabajo experimental, el compilador JIT copy-and-patch llega como funcionalidad activada por defecto. No va a hacer que Python sea tan rápido como C, porque nada lo hará sin compilación estática, pero las primeras pruebas de rendimiento muestran mejoras del 15-30% en código real, con patrones concretos que mejoran mucho más. Para un lenguaje cuya historia de rendimiento ha sido durante 30 años «reescribe el camino crítico en C», un JIT que acelera de forma notable el Python puro es un hito de verdad.
Por qué CPython nunca había tenido un JIT
No es que nadie lo intentara. PyPy lleva más de una década con un JIT y suele ejecutar código Python entre 5 y 10 veces más rápido que CPython. Pero PyPy es una implementación separada con su propio runtime, y nunca ha alcanzado la cuota de mercado de CPython, porque el ecosistema de extensiones en C (NumPy, pandas, scikit-learn, todo lo que hace de Python el lenguaje de la ciencia de datos) está ligado a la API C de CPython.
Meter un JIT en el propio CPython se ha propuesto e intentado varias veces. Los obstáculos están bien documentados. La arquitectura de CPython dificulta la compilación JIT: el bytecode es de tipado dinámico (el JIT necesita información de tipos para generar código eficiente, y Python no la proporciona de forma estática), la API C permite que el código C manipule objetos Python directamente de formas que rompen las suposiciones del JIT, y el recolector de basura por conteo de referencias del intérprete genera una sobrecarga de contabilidad que un JIT no puede eliminar fácilmente.
Intentos anteriores como Unladen Swallow (Google, 2009) y Pyston (Dropbox, 2014) trataron de acoplar un JIT basado en LLVM a CPython. Ambos descubrieron que la sobrecarga de compilación de LLVM era demasiado alta para las cargas típicas de Python. LLVM está diseñado para la compilación anticipada (AOT) de bases de código grandes; usarlo para compilar en JIT funciones Python cortas añade milisegundos de compilación para microsegundos de ejecución. La compilación costaba más de lo que la aceleración ganaba.
Copy-and-Patch: un tipo de JIT distinto
La técnica copy-and-patch, presentada en un artículo de investigación de 2021, adopta un enfoque fundamentalmente distinto para la compilación JIT. En lugar de traducir el bytecode a una representación intermedia y ejecutar pasadas de optimización (el enfoque de LLVM), copy-and-patch trabaja con plantillas de código precompiladas.
La idea es esta: para cada instrucción de bytecode (LOAD_FAST, BINARY_ADD, CALL_FUNCTION, etc.), el compilador precompila una implementación en C a código máquina, con «huecos» de marcador para datos específicos de cada variable: asignaciones de registros, valores constantes, desplazamientos de memoria. En tiempo de ejecución, la compilación JIT consiste simplemente en copiar la plantilla precompilada y rellenar los huecos con los valores concretos de esa función. Sin pasadas de optimización, sin algoritmos de asignación de registros, sin selección de instrucciones. Solo memcpy y parcheo.
Traditional JIT pipeline:
Python bytecode
→ Parse into IR
→ Type inference
→ Optimization passes (CSE, DCE, loop unrolling, inlining...)
→ Register allocation
→ Instruction selection
→ Machine code
Total: 1-100ms per function
Copy-and-patch pipeline:
Python bytecode
→ For each instruction, copy pre-compiled template
→ Fill in holes (constants, offsets)
→ Done — machine code ready
Total: 1-100μs per function (1000x faster compilation)
El precio está en la calidad del código. LLVM produce código máquina muy optimizado. Copy-and-patch produce código que es, esencialmente, una versión compilada del bucle del intérprete: cada instrucción de bytecode sigue siendo una plantilla separada, con muy poca optimización entre instrucciones. El código generado es mejor que la interpretación (sin sobrecarga de despacho, sin sentencia switch, con mejor predicción de saltos), pero peor de lo que produciría un compilador optimizador completo.
Para Python, este compromiso es excelente. Las funciones Python suelen ser cortas, se llaman muchas veces e individualmente tardan microsegundos. Un JIT que compila en microsegundos y acelera la ejecución un 20-30% vale más que uno que compila en milisegundos y la acelera un 200%, porque en el primer caso el coste de compilación se amortiza casi de inmediato.
Qué se vuelve más rápido
El JIT no acelera todo el código Python por igual, como por arte de magia. Para entender qué se beneficia más, hay que entender en qué gasta tiempo el intérprete.
Sobrecarga de despacho de bytecode. En el intérprete, cada instrucción de bytecode requiere: leer el siguiente opcode, decodificarlo y saltar al manejador mediante una sentencia switch. Esta sobrecarga puede suponer entre un 30 y un 50% del tiempo total en bucles ajustados. El JIT la elimina por completo: las instrucciones se compilan en código máquina secuencial con saltos directos.
Operaciones especializadas por tipo. Python 3.11 introdujo el intérprete adaptativo con especialización, que sustituye operaciones genéricas por versiones específicas de tipo tras observar qué tipos se usan de verdad. BINARY_ADD se convierte en BINARY_ADD_INT cuando ve dos enteros. El JIT compila estas instrucciones especializadas en código máquina eficiente: una suma de enteros pasa a ser una sola instrucción add en lugar de una llamada a función.
Predicción de saltos. El bucle central de despacho del intérprete, un switch con cientos de casos, es una pesadilla para el predictor de saltos de la CPU. El JIT lo sustituye por un flujo de control directo que la CPU predice con precisión. En las CPU modernas, donde un fallo de predicción cuesta entre 15 y 20 ciclos, esto por sí solo explica una parte importante de la mejora.
Lo que no se acelera: las llamadas a extensiones en C (NumPy, pandas), las operaciones de E/S (red, disco) y las operaciones dominadas por la reserva de memoria (crear millones de objetos pequeños). Si tu programa Python pasa el 95% del tiempo en extensiones C y el 5% en Python puro, el JIT acelera ese 5%: medible, pero nada transformador.
La cadena de especialización
El JIT no trabaja solo. Es la última etapa de una cadena de rendimiento que empezó con el intérprete especializado de Python 3.11 y continuó con las mejoras incrementales de las versiones 3.12 a 3.14.
- Nivel 0: Intérprete. Todo el código empieza aquí. Interpretación estándar del bytecode con especialización adaptativa. Tras varias llamadas a una función, las instrucciones más usadas se sustituyen por versiones especializadas por tipo.
- Nivel 1: Bytecode compilado con JIT. El JIT copy-and-patch compila el bytecode especializado a código máquina. Esto elimina la sobrecarga de despacho y habilita optimizaciones básicas como el plegado de constantes y la eliminación de código muerto dentro de las plantillas compiladas.
- Nivel 2 (futuro): Optimización basada en trazas. Registrar las trazas de ejecución de los caminos de código calientes y compilar trazas completas, cruzando los límites de las funciones, en código máquina optimizado. Está planeado, pero todavía no se distribuye.
Este enfoque por niveles es parecido al que usan YJIT de Ruby y otros runtimes de lenguajes modernos. Se empieza con una interpretación rápida, se pasa a una compilación rápida cuando el código se calienta y se reserva la optimización costosa para los caminos más calientes. Es la misma idea de fondo: la mayoría del código no merece optimizarse, así que hay que gastar el presupuesto de compilación en el código que más se ejecuta.
Impacto en memoria y arranque
Los compiladores JIT consumen memoria para el código compilado. La sobrecarga de memoria del JIT copy-and-patch es moderada: el código compilado es más grande que el bytecode, pero más pequeño que el que producen los JIT basados en LLVM (porque no hay hinchazón por optimización). La implementación actual usa entre 1,5 y 3 veces la memoria del bytecode que sustituye, y solo compila las funciones que se llaman con la frecuencia suficiente para merecerlo.
El tiempo de arranque preocupa en los scripts Python de vida corta. El JIT añade coste al cargar la biblioteca de plantillas y al preparar la infraestructura de compilación. En scripts que duran menos de un segundo, ese coste podría superar la mejora. CPython lo resuelve compilando funciones con JIT solo después de que se hayan llamado un número mínimo de veces: los scripts de vida corta se quedan en el intérprete y no pagan el impuesto del JIT.
Esto es configurable. El flag -X jit controla el comportamiento del JIT, y las variables de entorno ajustan el umbral de compilación. En funciones serverless y herramientas de línea de comandos, donde el arranque importa, puedes subir el umbral o desactivar el JIT del todo. En servidores de larga duración y scripts de procesamiento de datos, donde importa el rendimiento en régimen estable, los valores por defecto funcionan bien.
Qué significa para el ecosistema de Python
El JIT no cambia la posición de Python en la jerarquía de rendimiento: C, Rust, Go y Java siguen siendo mucho más rápidos en cargas intensivas en cómputo. Lo que cambia es el umbral a partir del cual un desarrollador Python necesita recurrir a esas alternativas.
Una aceleración del 20-30% en Python puro significa que algunas cargas que antes necesitaban extensiones en C o una reescritura ahora corren lo bastante rápido en Python puro. Los scripts de procesamiento de datos que tardaban 10 minutos tardan 7. Los servidores web que atendían 1000 peticiones por segundo atienden 1300. No son cifras revolucionarias, pero son la diferencia entre «Python va lo bastante rápido» y «hay que reescribir esto en Go».
Y lo más importante: la infraestructura del JIT sienta las bases para optimizaciones futuras. El enfoque copy-and-patch puede ampliarse con mejores plantillas, más especialización y, eventualmente, compilación basada en trazas. Cada versión de Python puede incorporar mejores plantillas sin cambiar la arquitectura fundamental del JIT. La mejora del 20-30% en 3.15 es un suelo, no un techo.
Tras tres décadas como uno de los lenguajes más populares y, a la vez, más lentos, CPython por fin invierte en serio en rendimiento. El JIT no va a convencer a quienes dicen que «Python es demasiado lento»; nada lo hará, porque en algunas cargas Python es realmente lento, con JIT o sin él. Pero para la gran mayoría del código Python, donde la velocidad era «suficiente, pero no buena», el JIT lo acerca a «realmente bueno». Es más importante de lo que parece.


