Python quitará su JIT: lo he encendido y medido en la 3.15

Publicado 28 octubre 2026 16 min de lectura
Ver imagen del artículo
Procesador Intel Core i5-8400 montado en el zócalo de una placa base Gigabyte

En la entrada sobre el lazy import de Python 3.15 dejé el JIT en la lista de lo que no había probado. Pensaba volver a él sin prisa, y ahora hay prisa.

El 19 de octubre, sus autores retiraron el PEP 836, la propuesta que debía convertir el JIT en una parte con soporte de CPython. Al día siguiente, Brett Cannon contestó en el mismo hilo que, sin ese PEP, el código sale de la rama principal.

Esquema de barras: cuánto cambia la velocidad de 20 pruebas de pyperformance al encender el JIT de Python 3.15.2. Richards va un 104,8 % más rápido y json_loads un 20,5 % más lento; media geométrica, +11,6 %. Debajo, un guion que suma un registro no cambia y un bucle de enteros va un 63 % más rápido
El JIT de Python 3.15.2 encendido frente a apagado, medido el 28/10/2026 con el Python de uv. Esquema propio.

Antes de que se vaya lo he encendido y lo he medido. No he compilado nada: el Python 3.15 que instala uv ya lo trae dentro, apagado, y se enciende con una variable de entorno. Lo que no esperaba es que, si systemd le niega memoria ejecutable, la 3.15 no vuelva al intérprete: aborta.

Lo Probado es del 28 de octubre de 2026, entre las 06:10 y las 07:08 UTC, en un Ubuntu 24.04 x86-64 de ocho hilos con carga de fondo, siempre en una carpeta propia. Lo del PEP, el foro de Python, LWN y los paquetes fuente de Debian y Ubuntu es Revisado: leído, no ejecutado (así los distingo).

[INFO] Nada de esto pide sudo ni toca el python3 del sistema. uv, su caché y el Python que baja se quedan en ~/jit315, y con --no-bin no deja enlaces en ~/.local/bin. pip, que usa pyperformance, sí deja su caché en ~/.cache/pip: lo cuento en la vuelta atrás. La prueba con systemd va en una unidad pasajera de mi usuario, que desaparece al terminar.

Seis meses de plazo y un PEP retirado

2024

Entra en CPython

Brandt Bucher lo añade a principios de 2024 (PR 113465) y sale con la 3.13 como experimento. Solo lo explica un PEP informativo, el 744.

05/06/2026

El consejo lo para

El consejo de dirección pide un PEP que se pueda aceptar o rechazar, y que no entre nada nuevo del JIT en main mientras tanto. Da seis meses: si no hay PEP aceptado, el código sale de main.

03/07/2026

PEP 836

Savannah Ostrowski, Ken Jin y Brandt Bucher lo publican. Su meta: un 20 % más de media geométrica en pyperformance para la compilación sin GIL con JIT, frente a la misma sin JIT, en la primera beta de la 3.17.

21/09/2026

La respuesta del consejo

Thomas Wouters publica la postura del consejo, sin Ostrowski, que es coautora del PEP y miembro del consejo. Para el consejo, el PEP responde a lo que pidió, «although perhaps some less convincingly than others». Pero es «vague and optimistic» con la memoria, deja la revisión de compatibilidad «way, way too late» y trata la seguridad como un área «underserved».

19/10/2026

Retirado

Ostrowski lo retira porque no tienen «the personal bandwidth» para sacarlo adelante. Dejarán la implementación de referencia enlazada desde el PEP.

20/10/2026

Fuera de main

Cannon, a la pregunta de si el trabajo se para: «In CPython’s repo, yes». Lo que siga, fuera.

LWN lo contó el 26 de octubre: lo esperable es quitarlo pronto, al principio del ciclo de la 3.16, para que lo que se rompa salga cuanto antes. A las 07:10 UTC de hoy, Tools/jit seguía en main y ninguna PR lo quitaba (mirado con la API de GitHub).

Sale de main; la 3.14 y la 3.15, sin noticias

Lo que sale es main, la futura 3.16. No he encontrado nada sobre quitarlo de la 3.14 o la 3.15, y en junio, al pararlo, el consejo dejó seguir los arreglos de fallos y de seguridad.

Pero el mismo 19, poco después de retirar el PEP, Ostrowski se quitó de CODEOWNERS (a quién se pide revisión) del JIT y del optimizador, en main y en las ramas 3.15 y 3.14 (PR 159775). Cannon dice que el trabajo se para en el repositorio; que sigan llegando arreglos está por ver.

Quién lo trae compilado y apagado

La clave es la opción yes-off de configure: el JIT se compila, pero arranca apagado, y PYTHON_JIT=1 lo enciende. Según el propio PEP 836, así salen los binarios oficiales de Windows y macOS, y también los de Fedora y Gentoo. He mirado lo que tengo más a mano:

Python¿JIT compilado?Cómo lo he visto
uv 0.13.6: 3.13.18, 3.14.10 y 3.15.2sí, apagadoinstalados en una carpeta y preguntados (Probado)
python3.14 de Ubuntu 26.04 LTS, 3.14.4-1ubuntu0.2 (amd64)sí, apagado.deb desempaquetado sin instalar, sha256 contra el índice (Probado)
python3.15 de Ubuntu 26.10, 3.15.0-1ubuntu1sí, en amd64 y arm64paquete fuente: debian/rules y changelog (Revisado)
python3.15 de Debian sid, 3.15.2-1sí, en amd64 y arm64debian/rules (Revisado)
python3.12 de Ubuntu 24.04 LTSno: el JIT llegó en la 3.13el del sistema (Probado)
A 28/10/2026. «Apagado» es --enable-experimental-jit=yes-off. Los binarios de Windows y macOS de python.org, según el PEP 836; no los he comprobado.

Cómo saber si el tuyo lo lleva

Basta con preguntarle. En la 3.13 todavía no existe sys._jit, así que la primera orden es la que vale para todos:

comprobar.sh
BASH
# ¿Este python3 trae el JIT compilado? Vale de la 3.13 en adelante.
python3 -c "import sysconfig; print(sysconfig.get_config_var('CONFIG_ARGS'))" \
  | grep -o -e "--enable-experimental-jit[^' ]*" || echo "sin JIT"

# Desde la 3.14, sys._jit dice si está disponible y si está encendido
python3 -c "import sys; print(sys._jit.is_available(), sys._jit.is_enabled())"
PYTHON_JIT=1 python3 -c "import sys; print(sys._jit.is_available(), sys._jit.is_enabled())"

Con el python3 de Ubuntu 24.04 sale «sin JIT» y dos AttributeError, en los que la 3.12, muy servicial, pregunta si quería decir _git. Con el de uv delante en el PATH salen --enable-experimental-jit=yes-off, True False y True True: está, viene apagado y la variable lo enciende.

Un detalle tonto: python3.15 --help-env lista PYTHON_PERF_JIT_SUPPORT, que es para el perfilador perf, pero no PYTHON_JIT. La variable está documentada desde la 3.13 en la página de la línea de órdenes; en la ayuda del propio binario no aparece.

[WARN] Es experimental. En septiembre el consejo escribió que el PEP era «overly optimistic» sobre las diferencias de comportamiento con el JIT encendido y que dejaba poco atendida la seguridad. Yo lo enciendo para medir, en una carpeta. En un servicio no lo pondría, y más abajo hay otro motivo.

Encenderlo con el Python de uv

uv 0.13.6, con fecha del 26 de octubre en sus notas, trae la 3.15.2, que python.org publicó el 25. Bajo uv, compruebo su sha256 y le pido la 3.15, todo dentro de la carpeta:

instalar.sh
BASH
# Todo en una carpeta propia: uv, su caché y los Python que baje. Sin sudo.
mkdir -p ~/jit315 && cd ~/jit315
export UV_CACHE_DIR=$PWD/cache UV_PYTHON_INSTALL_DIR=$PWD/python
V=0.13.6
U=https://github.com/astral-sh/uv/releases/download/$V/uv-x86_64-unknown-linux-gnu.tar.gz
curl -sSfL -O $U -O $U.sha256
sha256sum -c uv-x86_64-unknown-linux-gnu.tar.gz.sha256
tar xzf uv-x86_64-unknown-linux-gnu.tar.gz
UV=$PWD/uv-x86_64-unknown-linux-gnu/uv

# --no-bin: que no deje enlaces en ~/.local/bin ni toque el PATH
$UV python install --no-bin 3.15
PY=$($UV python find 3.15)
$PY -c "import sys; print(sys.version.split()[0], sys._jit.is_available(), sys._jit.is_enabled())"
PYTHON_JIT=1 $PY -c "import sys; print(sys._jit.is_enabled())"

Las dos últimas líneas dan 3.15.2 True False y True. Lo que baja uv es la compilación 20261026 de python-build-standalone, sin símbolos, y su configure lleva el JIT en yes-off junto a PGO, LTO, BOLT y el intérprete de llamadas de cola.

No es el Python del sistema, y se nota: con los .pth de LiteLLM ya vi que dos Python de la misma máquina no arrancan igual.

Medirlo sin engañarme

Mi primera medida decía que el JIT hacía Python un 5 % más lento. Pasé 22 pruebas con el JIT apagado y después las mismas encendido, y entre el principio de una tanda y el final de la otra la carga subió de 1,9 a 3,9. No estaba sola, y sospecho que eso pesaba más que el JIT.

Así que las he intercalado: tres vueltas, cada una con el JIT apagado y luego encendido, sumadas en dos ficheros con --append. Son 14 pruebas de la suite (20 resultados, porque deepcopy y scimark dan varios), para que quepa en un cuarto de hora:

pyperformance.sh
BASH
# Mismo binario, JIT apagado y encendido. Sigue en ~/jit315, con $PY de instalar.sh
cd ~/jit315
$PY -m venv pp && pp/bin/pip install -q pyperformance==1.14.0
B=chaos,coroutines,crypto_pyaes,deepcopy,float,generators,go,json_loads
B=$B,nbody,raytrace,regex_v8,richards,scimark,spectral_norm

# Tres vueltas alternando 0 y 1: la carga de fondo se reparte entre los dos.
# --inherit-environ: sin él, pyperf no pasa PYTHON_JIT a los procesos que miden
for vuelta in 1 2 3; do
  for j in 0 1; do
    PYTHON_JIT=$j nice -n 10 pp/bin/pyperformance run --fast \
      --inherit-environ PYTHON_JIT -p $PY -b $B --append jit$j.json
  done
done
pp/bin/python -m pyperf compare_to jit0.json jit1.json --table

Ojo con --inherit-environ. pyperf mide en procesos nuevos y solo les pasa unas pocas variables: en la 2.10.0, PYTHON_GIL y PYTHON_CPU_COUNT sí, PYTHON_JIT no.

Sin esa opción, sys._jit.is_enabled() daba False dentro del proceso que mide aunque PYTHON_JIT=1 estuviera puesto. Acabas comparando el JIT apagado consigo mismo, y pyperf solo avisa de que el resultado puede ser inestable.

Lo que da con el JIT encendido

+11,6 %

MEDIA GEOMÉTRICA

tres vueltas, medias

11 de 20

MEJORAN

las otras nueve, no

41,5 → 20,2 ms

RICHARDS

la que más gana

28,1 → 35,3 µs

JSON_LOADS

la que más pierde aquí

Tomando el mínimo de cada prueba en vez de la media, sale un 14,2 % y mejoran trece de veinte.

Un 11,6 % con letra pequeña

Intercalar no quita la carga de fondo, la reparte. La segunda vuelta con el JIT apagado cayó en mal momento: raytrace tardó 350,7 ms de media, por 232,6 y 239,9 en las otras dos. Vuelta a vuelta, la media geométrica sale +1,7 %, +22,3 % y +11,0 %. Mi cifra quiere decir «entre poco y bastante».

Media hora después repetí cuatro de esas pruebas, con el mismo bloque y dos vueltas en lugar de tres. Lo que da compare_to, JIT encendido frente a apagado:

PruebaTres vueltas, 06:26-06:40Dos vueltas, 06:59-07:02
richards2,05 veces más rápida2,67 veces más rápida
float1,46 veces más rápida1,25 veces más rápida
raytrace1,03 veces más lenta1,04 veces más rápida
json_loads1,26 veces más lenta1,17 veces más rápida
pyperformance 1.14.0 --fast con el Python 3.15.2 de uv, 28/10/2026 (UTC), con carga de fondo en las dos tandas.

Lo que gana mucho repite. Lo que pierde, al menos json_loads, puede ser ruido.

El What’s New de la 3.15 da un 7-8 % en x86-64 con Linux frente al intérprete normal; el apéndice del PEP 836, un 6,9 % en ripley, un Core i5-8400. Lo mío son 14 pruebas en modo rápido, con carga y frente al intérprete de llamadas de cola. Va en la misma dirección, y con eso me conformo.

Un guion de registros apenas lo nota

En su respuesta de septiembre, el consejo recordaba que pyperformance «can severely underrepresent common uses of Python». Por eso he medido dos guiones fuera de la suite. El primero suma por código y por ruta un registro de acceso inventado de 500.000 líneas:

contar_log.py
PYTHON
# Lo que hacen muchos guiones de administración: leer un registro y sumar por código y por ruta
import sys
from collections import Counter

codigos, bytes_ruta = Counter(), Counter()
with open(sys.argv[1]) as f:
    for linea in f:
        partes = linea.split('"')
        ruta = partes[1].split()[1]
        codigo, tam = partes[2].split()
        codigos[codigo] += 1
        bytes_ruta[ruta] += int(tam)
print(codigos.most_common(3), bytes_ruta.most_common(1))

El segundo es un bucle de enteros, justo el tipo de código en el que un JIT debería notarse:

bucle.py
PYTHON
# Un bucle apretado de enteros (Collatz hasta 400.000): donde un JIT debería notarse
def pasos(n):
    p = 0
    while n != 1:
        n = n // 2 if n % 2 == 0 else 3 * n + 1
        p += 1
    return p

print(sum(pasos(i) for i in range(1, 400_000)))

Cinco vueltas alternas, con el tiempo y la memoria máxima de /usr/bin/time:

guiones.sh
BASH
# Cinco vueltas alternas: tiempo en segundos y memoria máxima en KiB
cd ~/jit315
for vuelta in 1 2 3 4 5; do
  for j in 0 1; do
    for g in "contar_log.py acceso.log" "bucle.py"; do
      PYTHON_JIT=$j nice -n 10 /usr/bin/time -f "jit=$j $g: %e s %M KiB" $PY $g >/dev/null
    done
  done
done
GuionJIT apagadoJIT encendidoMemoria máxima
contar_log.py0,55 s0,56 s12.032 → 13.312-13.568 KiB
bucle.py3,15 s1,93 s11.392 → 12.800-12.928 KiB
contar_log.py, repetido0,56 s0,51 s11.904-13.440 → 13.440-13.568 KiB
bucle.py, repetido2,91 s1,81 s11.392 → 12.672-12.800 KiB
Medianas de cinco vueltas alternas con el Python 3.15.2 de uv, 28/10/2026: de 06:40 a 06:41 UTC y, repetido, de 06:58 a 06:59.

Con el registro, la diferencia es de centésimas y cambia de lado de una tanda a otra; el bucle tarda casi un 40 % menos las dos veces. Sospecho que buena parte de los guiones de administración se parecen más al primero que al segundo: leer, partir cadenas, contar. Ahí, al menos en esta prueba, el JIT no tiene dónde rascar.

En memoria máxima, el JIT encendido pide de 1,25 a 1,6 MiB más en casi todas las ejecuciones; en la repetición, una vuelta del registro con el JIT apagado ya llegó a 13.440 KiB. Es poco, pero son justo las medidas que el consejo echaba en falta en el PEP.

Con MemoryDenyWriteExecute, el proceso aborta

Según su manual, la opción de endurecimiento MemoryDenyWriteExecute= de systemd rechaza las llamadas a mprotect() que piden permiso de ejecución, y es incompatible con «JIT execution engines».

El JIT de CPython escribe su código en memoria de lectura y escritura y luego la pasa a lectura y ejecución con mprotect(). En Python/jit.c hay un comentario que lo dice en mayúsculas: «Do NOT ever leave the memory writable!». Lo he juntado todo en una unidad pasajera de mi usuario:

systemd.sh
BASH
# Un bucle caliente basta para que el JIT quiera compilar
cd ~/jit315
cat > caliente.py <<'FIN'
import sys
t = 0
for i in range(3_000_000):
    t += i % 7
print(sys.version.split()[0], "JIT encendido:", sys._jit.is_enabled())
FIN

# Unidad pasajera del usuario, con el endurecimiento y el JIT encendido.
# La unidad no arranca en esta carpeta: la ruta del guion, entera.
systemd-run --user --wait --pipe --collect \
  -p MemoryDenyWriteExecute=yes -E PYTHON_JIT=1 $PY $PWD/caliente.py

Sale esto (la ruta, recortada), y el proceso aborta en la línea del bucle:

salida
SALIDA
Fatal Python error: make_cold_executor: Cannot allocate core JIT code
Python runtime state: initialized
RuntimeWarning: JIT unable to protect executable memory (13)

Current thread 0x00007fe247e57b80 [python3.15] (most recent call first):
  File "/home/…/jit315/caliente.py", line 4 in <module>
Finished with result: core-dump
Main processes terminated with: code=dumped/status=ABRT

El 13 es EACCES, permiso denegado: el mprotect() que pide ejecución no pasa. Con PYTHON_JIT=0 en la misma unidad el guion termina bien, y con el JIT encendido sin la opción, también. La 3.14.10 de uv no vuelca memoria: el aviso sale como excepción y el proceso acaba con código 1. Muere igual.

Si lo tienes encendido

PYTHON_JIT viaja por el entorno, así que la heredan los procesos hijos. Hay que buscarla donde se ponen variables: unidades de systemd, Dockerfile, ficheros .env. Y si la unidad lleva además MemoryDenyWriteExecute=yes, el servicio no cae al arrancar, sino cuando el JIT intenta compilar el primer código caliente.

buscar.sh
BASH
# ¿Quién la pone? Unidades del sistema y del usuario
grep -rn "PYTHON_JIT" /etc/systemd/system ~/.config/systemd/user 2>/dev/null

# Las dos cosas de una unidad concreta, de una vez. EnvironmentFiles también:
# lo que llega desde un fichero no sale en Environment
systemctl show -p Environment,EnvironmentFiles,MemoryDenyWriteExecute nombre.service

La segunda orden la he probado con --user en dos unidades pasajeras. Con -E PYTHON_JIT=1 devuelve Environment=PYTHON_JIT=1 y MemoryDenyWriteExecute=yes. Con la variable en un EnvironmentFile=, Environment= sale vacío y el guion aborta igual. Solo EnvironmentFiles= dice de dónde viene, y el grep tiene que llegar a ese fichero.

Cuando el JIT salga de main, sospecho que la variable se quedará en nada: el python3.12 de Ubuntu 24.04, que nunca tuvo JIT, arranca con PYTHON_JIT=1 sin decir ni pío. Lo que haga la 3.16 no lo sé; lo veré cuando salga la primera versión sin JIT.

Vuelta atrás

No hay nada instalado fuera de la carpeta. Lo único que queda fuera es la caché de pip: con una carpeta personal vacía, pyperformance dejó 11 MB en ~/.cache/pip.

deshacer.sh
BASH
# Apagarlo: basta con quitar la variable (o ponerla a 0)
unset PYTHON_JIT

# Borrar la prueba entera: uv, su caché, el Python 3.15 y el entorno de pyperformance
rm -rf ~/jit315

# La caché de pip queda fuera. Es solo caché: borrarla
# obliga a pip a volver a descargar, nada más
rm -rf ~/.cache/pip

Si alguien había puesto Environment=PYTHON_JIT=1 en una unidad, se quita la línea y después systemctl daemon-reload y un reinicio del servicio. Eso no lo he probado: mis pruebas solo usan unidades pasajeras.

Qué no he probado

  • La compilación sin GIL con el JIT, que es donde el PEP 836 ponía su objetivo del 20 %.
  • La suite entera de pyperformance en modo normal y en una máquina sin carga: lo mío son 14 pruebas en modo rápido.
  • ARM, macOS y los binarios de python.org para Windows y macOS.
  • El python3.14 de Ubuntu 26.04 en marcha: solo he leído su configuración dentro del .deb, porque esta máquina es 24.04.
  • Un servicio con días de marcha: su memoria, y perf o gdb con marcos del JIT.

Cuando entre en main la PR que quite el JIT, lo apuntaré aquí con la fecha. La foto de portada es un Intel Core i5-8400 de Oligopolism (CC0, vía Wikimedia Commons), el mismo modelo que ripley; no es esa máquina ni la mía.

Compartir esta entrada WhatsApp X / Twitter LinkedIn Reddit Telegram Bluesky Email
Siguiente comando: volver al archivo o pulsa Ctrl + K
Hilo abierto

La conversación empieza aquí

Escribir un comentario

Nadie ha escrito todavía. Si lo has probado en tu máquina, cuenta qué te salió: otra versión u otro resultado también ayudan.

Escribe tu comentario

Si planteas un problema técnico, di sistema, versión, qué esperabas y qué has probado. Respeto y cero spam: tu correo no se publica.

0 caracteresCtrl + Enter para enviar

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.