- Resultados de benchmarks que miden de forma sistemática las cifras de rendimiento de operaciones, memoria y E/S en Python, cuantificando el tiempo y el uso de memoria de cada operación
- En velocidad, presenta latencias relativas de varias operaciones, como acceso a atributos 14ns, agregar a una lista 29ns, abrir un archivo 9μs y respuesta de FastAPI 8.6μs
- En memoria, muestra cifras concretas como cadena vacía 41 bytes, entero 28 bytes, lista vacía 56 bytes, diccionario vacío 64 bytes y proceso vacío 16MB
- Compara las diferencias de rendimiento entre la biblioteca estándar y bibliotecas alternativas como
orjson y msgspec en áreas como estructuras de datos, serialización y procesamiento asíncrono
- Como lecciones clave, destaca el alto overhead de memoria de los objetos de Python, las consultas rápidas en dict/set, el efecto de ahorro de memoria de
__slots__ y la necesidad de reconocer el overhead del procesamiento asíncrono
Resumen general
- Material que organiza los indicadores de rendimiento que un desarrollador de Python debería conocer, presentando valores medidos reales de velocidad de operaciones y uso de memoria
- Los benchmarks se ejecutaron en CPython 3.14.2, Mac Mini M4 Pro (ARM, 14 núcleos, 24GB RAM)
- Los resultados se centran en la comparación relativa, y el código y los datos están disponibles públicamente en un repositorio de GitHub
Uso de memoria (Memory Costs)
- Un proceso de Python vacío usa 15.73MB de memoria
- Las cadenas tienen una base de 41 bytes, más 1 byte adicional por carácter
- Ej.: cadena vacía 41B, cadena de 100 caracteres 141B
- Los tipos numéricos: enteros pequeños (0–256) 28B, enteros grandes (1000) también 28B, enteros muy grandes (10ⁱ⁰⁰) 72B, punto flotante 24B
- Tamaño base de colecciones: lista 56B, diccionario 64B, conjunto 216B
- Con 1,000 elementos: lista 35.2KB, diccionario 63.4KB, conjunto 59.6KB
- Instancias de clase: clase normal (5 atributos) 694B, clase con
__slots__ 212B
- Con 1,000 instancias: clase normal 165.2KB, clase con
__slots__ 79.1KB
Operaciones básicas (Basic Operations)
- Operaciones aritméticas: suma de enteros 19ns, suma de flotantes 18.4ns, multiplicación de enteros 19.4ns
- Operaciones con cadenas: concatenación 39.1ns, f-string 64.9ns,
.format() 103ns, formato con % 89.8ns
- Operaciones con listas:
append() 28.7ns, comprensión de listas (1,000 elementos) 9.45μs, bucle for equivalente 11.9μs
- La comprensión de listas es aproximadamente un 26% más rápida que el bucle for
Acceso e iteración en colecciones (Collection Access and Iteration)
- Acceso por clave/índice: consulta en diccionario 21.9ns, membresía en conjunto 19ns, acceso por índice en lista 17.6ns
- La membresía en lista (1,000 elementos) tarda 3.85μs, unas 200 veces más lenta que set/dict
- Comprobación de longitud:
len() en lista 18.8ns, diccionario 17.6ns, conjunto 18ns
- Iteración: lista (1,000 elementos) 7.87μs, diccionario 8.74μs,
sum() 1.87μs
Clases y atributos (Class and Object Attributes)
- Velocidad de acceso a atributos: tanto la clase normal como la clase con
__slots__ leen en 14.1ns y escriben en alrededor de 16ns
- Otras operaciones: lectura de
@property 19ns, getattr() 13.8ns, hasattr() 23.8ns
- Al usar
__slots__, el ahorro de memoria supera 2 veces, mientras que la velocidad de acceso se mantiene en un nivel similar
JSON y serialización (JSON and Serialization)
- Rendimiento de bibliotecas alternativas frente a la biblioteca estándar
orjson serializa objetos complejos en 310ns, más de 8 veces más rápido que json, que tarda 2.65μs
msgspec marca 445ns, ujson 1.64μs
- En deserialización,
orjson también es el más rápido con 839ns
- Pydantic:
model_dump_json() 1.54μs, model_validate_json() 2.99μs
Frameworks web (Web Frameworks)
- Con la misma respuesta JSON: FastAPI 8.63μs, Starlette 8.01μs, Litestar 8.19μs, Flask 16.5μs, Django 18.1μs
- FastAPI responde aproximadamente 2 veces más rápido que Django
Entrada/salida de archivos (File I/O)
- Abrir y cerrar archivo 9.05μs, leer 1KB 10μs, leer 1MB 33.6μs
- Escritura: 1KB 35.1μs, 1MB 207μs
- Pickle es aproximadamente 2 veces más rápido que
json tanto al serializar como al deserializar (pickle.dumps() 1.3μs, json.dumps() 2.72μs)
Base de datos y caché (Database and Persistence)
- SQLite: insert 192μs, select 3.57μs, update 5.22μs
- diskcache: set 23.9μs, get 4.25μs
- MongoDB: insert 119μs, find_one 121μs
- SQLite es el más rápido en lectura, mientras que diskcache destaca en rendimiento de escritura
Llamadas a función y excepciones (Function and Call Overhead)
- Llamadas a función: función vacía 22.4ns, método 23.3ns, lambda 19.7ns
- Manejo de excepciones: try/except (sin excepción) 21.5ns, con excepción 139ns
- Comprobación de tipos:
isinstance() 18.3ns, comparación con type() 21.8ns
Overhead asíncrono (Async Overhead)
- Creación de corrutina 47ns,
run_until_complete 27.6μs
asyncio.sleep(0) 39.4μs, gather(10 coroutines) 55μs
- Frente a una llamada de función síncrona (20ns), la ejecución asíncrona (28μs) es unas 1,000 veces más lenta
Lecciones clave (Key Takeaways)
- El overhead de memoria de los objetos de Python es alto; incluso una lista vacía usa 56 bytes
- Las consultas en diccionarios y conjuntos son cientos de veces más rápidas que buscar en una lista
- Bibliotecas JSON alternativas como
orjson y msgspec son entre 3 y 8 veces más rápidas que la estándar
- El procesamiento asíncrono tiene un overhead alto, por lo que se recomienda usarlo solo cuando se necesita paralelismo
__slots__ reduce la memoria a menos de la mitad casi sin pérdida de rendimiento
1 comentarios
Comentarios en Hacker News
Mucha gente dice que si te importa la latencia en Python, deberías usar otro lenguaje, pero no estoy de acuerdo.
Bases de código enormes como Instagram, Dropbox y OpenAI también crecieron con Python. Al final aparecen problemas de rendimiento, y lo importante es tener la capacidad de resolverlos dentro de Python sin tener que migrar a otro lenguaje.
La mayoría de los problemas de rendimiento no vienen de los límites del lenguaje, sino de código ineficiente. Por ejemplo, un loop que repite 10 mil veces llamadas a funciones innecesarias.
También vale la pena revisar el Python latency quiz que hice.
Irónicamente, en el momento en que estos números se vuelven importantes, Python deja de ser la herramienta adecuada para ese trabajo.
En la práctica, lo importante es instrumentar el código y encontrar los cuellos de botella con herramientas como pyspy. Si ya estás preocupado por la velocidad de agregar elementos a una lista, entonces esa operación no debería hacerse en Python.
Este enfoque es posible gracias a la interoperabilidad entre Python y C. Zig también está mejorando bastante. No controlaría un avión con Python, pero seguir teniendo noción de los recursos importa.
Saber cuántos bytes ocupa una cadena vacía no sirve de mucho. Lo importante es entender la complejidad temporal y espacial.
Más que saber que un
intocupa 28 bytes, importa poder juzgar si un programa cumple con los requisitos de rendimiento y, si no, encontrar un mejor algoritmo.Por ejemplo, el hecho de que la concatenación de strings sea O(n²) también influye en el diseño de los f-strings de Python.
Que los diccionarios sean rápidos también explica por qué se usan tanto en todo Python.
Este tipo de cifras sirve para justificar con números ese conocimiento implícito.
intocupe 28 bytes sí importa en problemas donde hay que crear grandes cantidades de objetos.Me recuerda este artículo sobre el problema que tuvo Eric Raymond al migrar GCC con Reposurgeon.
El título es confuso, porque en realidad es una parodia del artículo de Jeff Dean de 2012, “Latency Numbers Every Programmer Should Know”.
Este tipo de juegos con títulos es común en papers de CS.
Era material interno para el diseño de RAM vs Disk del buscador en los inicios de Google.
Después los valores cambiaron con la llegada de la memoria flash, y también hay una anécdota de que Jeff creó un algoritmo de compresión para servir datos genómicos directamente desde flash.
La mayoría de los desarrolladores de Python debería enfocarse en cosas más importantes que estos detalles de rendimiento de bajo nivel.
Este tipo de material está bien como referencia, pero en la práctica rara vez hace falta.
La explicación sobre el tamaño de los strings está mal. Python tiene tres tipos de strings que usan 1, 2 o 4 bytes por carácter.
Para más detalle, revisa este blog.
El título y los ejemplos del artículo son algo imprecisos.
Por ejemplo, decir que “
item in setes 200 veces más rápido queitem in list” habla de una prueba de pertenencia, no de comparar velocidad de iteración.Aun así, en general el formato y la estructura son atractivos.
Falta medir el tiempo de creación de instancias de clases.
Después de refactorizar mi código y cambiar una estructura simple basada en listas por clases, el tiempo de ejecución pasó de varios microsegundos a varios segundos.
Me gustaría que midieran casos así.
Quizá el problema sea el abuso de clases. A veces una estructura simple con listas funciona mejor.
Más bien parece que pudo haber un mal uso de la programación orientada a objetos.
Sería buena idea subir el código a StackOverflow o a CodeReview.SE para recibir feedback.
Leí este artículo con la idea de preguntarme si hay algo fundamentalmente mal en el Python moderno.
Pero no estoy de acuerdo con la afirmación de que uno “deba saber” todos estos números.
Basta con tener una intuición general de unas cuantas operaciones clave.
El rango de caché de los small int en Python no es de 0 a 256, sino de -5 a 256.
Por eso a los principiantes a menudo les confunde mezclar identidad (
is) con igualdad (==).