38 puntos por GN⁺ 2026-01-02 | 1 comentarios | Compartir por WhatsApp
  • 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

 
GN⁺ 2026-01-02
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.

    • Yo me encargo de la optimización de rendimiento de sistemas escritos en Python. Pero estos números no significan mucho hasta que de verdad se vuelven un problema. Cuando pasa, lo mido directamente. Si escribes código intentando ahorrar llamadas a métodos, terminas perdiendo las ventajas de Python.
    • Python es lento incluso en operaciones básicas. Es lento en tareas simples como llamadas a funciones o acceso a diccionarios. En realidad, Python ha sobrevivido gracias a las bibliotecas basadas en C/C++ como Numpy.
    • Estos números no son un problema exclusivo de Python. En Zig también se consideran los ciclos de CPU o los fallos de caché. Todos los lenguajes tienen latencias para ciertas operaciones. Puede haber razones para no usar Python, pero esta no es una de ellas.
    • Algunas operaciones mejoran si usas módulos alternativos. Conocer ese tipo de cosas es importante, pero quien realmente lo necesita probablemente ya lo sabe. Aun así, Python es un lenguaje excelente para prototipado.
    • Nuestro sistema de build también está hecho en Python, así que quiero mantener Python mientras mejoramos el rendimiento. Por eso estos números sí son muy importantes.
  • Irónicamente, en el momento en que estos números se vuelven importantes, Python deja de ser la herramienta adecuada para ese trabajo.

    • Un enfoque realista es mantener el código en Python y bajar solo las partes críticas de rendimiento a extensiones en C o Rust. Así funcionan numpy, pandas y PyTorch.
      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.
    • Llevo 20 años trabajando con Python y nunca he necesitado saber estos números. En cambio, lo resuelvo con herramientas como profiling, Cython, SWIG y JIT.
    • Si una aplicación depende tanto de estos números, entonces Python es un lenguaje demasiado de alto nivel como para optimizarlo bien.
    • Pero yo sí construí pipelines de datos a gran escala con Python. Con la combinación de turbodbc + pandas obtengo velocidad de nivel C++. Usa más memoria, pero considerando el costo de la mano de obra, es mucho más eficiente.
      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.
    • Estos números son un último recurso. Solo vale la pena considerarlos después de resolver los cuellos de botella habituales como I/O de disco, red o complejidad algorítmica.
  • 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 int ocupa 28 bytes, importa poder juzgar si un programa cumple con los requisitos de rendimiento y, si no, encontrar un mejor algoritmo.

    • Pero el rendimiento siempre es una abstracción con fugas. Seamos conscientes o no, afecta todo el código.
      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.
    • Que un int ocupe 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.

    • Si alguien escribiera un paper titulado “latency numbers considered harmful is all you need”, seguro sería un éxito total en la academia.
    • Pero da la impresión de que el autor de este texto lo escribió en serio. No es que los lectores hayan malinterpretado el título.
    • Para que el título funcionara, los números tendrían que ser realmente útiles, pero son demasiados y poco prácticos.
    • Como referencia, parece que el texto original de Jeff Dean se escribió mucho antes de 2012.
      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.

    • Aun así, siempre tiene valor contar con conocimiento general sobre las herramientas que usas. Es capital intelectual y en ciertas situaciones puede ayudar bastante.
    • Cuando llegas a un límite, puedes buscar un módulo implementado en C o escribir uno tú mismo. Así es como Python ha evolucionado desde el principio.
    • Yo también suelo trabajar con la idea de que “es lo bastante rápido” en la mayoría de los casos. Este material me ayudó a confirmar con números esa intuición.
  • 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 set es 200 veces más rápido que item 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í.

    • Me recordó al chiste del médico: “si eso te duele, entonces no lo hagas”.
      Quizá el problema sea el abuso de clases. A veces una estructura simple con listas funciona mejor.
    • La creación de instancias de clases por sí sola normalmente no es un problema de rendimiento.
      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 (==).

    • Java también tiene un comportamiento parecido. Para la gente que empieza puede ser confuso.