38 points par GN⁺ 2026-01-02 | 1 commentaires | Partager sur WhatsApp
  • Résultats de benchmark mesurant de façon systématique les performances en calcul, mémoire et entrées/sorties de Python, avec une quantification du temps et de la mémoire consommée par chaque opération
  • Du point de vue de la vitesse, l’article présente les latences relatives de diverses opérations : accès à un attribut en 14 ns, ajout à une liste en 29 ns, ouverture d’un fichier en 9 μs, réponse FastAPI en 8,6 μs, etc.
  • Du point de vue de la mémoire, il donne des chiffres précis : chaîne vide 41 octets, entier 28 octets, liste vide 56 octets, dictionnaire vide 64 octets, processus vide 16 MB, etc.
  • Il compare, selon les domaines comme les structures de données, la sérialisation et le traitement asynchrone, les écarts de performances entre la bibliothèque standard et des bibliothèques alternatives (orjson, msgspec, etc.)
  • Parmi les enseignements clés : l’importante surcharge mémoire des objets Python, la rapidité des recherches dans les dict/set, l’effet de réduction mémoire de __slots__, et la nécessité de prendre en compte le surcoût de l’asynchrone

Vue d’ensemble

  • Document récapitulant les indicateurs de performance que les développeurs Python devraient connaître, avec des mesures réelles de vitesse d’exécution et d’usage mémoire
  • Les benchmarks ont été réalisés dans un environnement CPython 3.14.2, Mac Mini M4 Pro (ARM, 14 cœurs, 24 GB de RAM)
  • Les résultats mettent l’accent sur la comparaison relative, et le code comme les données sont publiés dans un dépôt GitHub

Utilisation mémoire (Memory Costs)

  • Un processus Python vide consomme 15,73 MB de mémoire
  • Les chaînes de caractères ont une base de 41 octets, avec 1 octet supplémentaire par caractère
    • Ex. : chaîne vide 41B, chaîne de 100 caractères 141B
  • Les types numériques : petits entiers (0–256) 28B, grands entiers (1000) 28B aussi, très grands entiers (10ⁱ⁰⁰) 72B, flottants 24B
  • Taille de base des collections : liste 56B, dictionnaire 64B, set 216B
    • Avec 1 000 éléments : liste 35,2 KB, dictionnaire 63,4 KB, set 59,6 KB
  • Instances de classes : classe classique (5 attributs) 694B, classe avec __slots__ 212B
    • Pour 1 000 instances : classe classique 165,2 KB, classe __slots__ 79,1 KB

Opérations de base (Basic Operations)

  • Opérations arithmétiques : addition d’entiers 19 ns, addition de flottants 18,4 ns, multiplication d’entiers 19,4 ns
  • Opérations sur les chaînes : concaténation 39,1 ns, f-string 64,9 ns, .format() 103 ns, formatage % 89,8 ns
  • Opérations sur les listes : append() 28,7 ns, compréhension de liste (1 000 éléments) 9,45 μs, boucle for équivalente 11,9 μs
    • La compréhension de liste est environ 26 % plus rapide qu’une boucle for

Accès et itération sur les collections (Collection Access and Iteration)

  • Accès par clé/index : recherche dans un dictionnaire 21,9 ns, test d’appartenance dans un set 19 ns, accès par index dans une liste 17,6 ns
    • Le test d’appartenance dans une liste (1 000 éléments) prend 3,85 μs, soit environ 200 fois plus lent que dans un set/dictionnaire
  • Vérification de longueur : len() prend 18,8 ns pour une liste, 17,6 ns pour un dictionnaire, 18 ns pour un set
  • Itération : liste (1 000 éléments) 7,87 μs, dictionnaire 8,74 μs, sum() 1,87 μs

Classes et attributs (Class and Object Attributes)

  • Vitesse d’accès aux attributs : lecture à 14,1 ns aussi bien pour une classe classique que pour une classe __slots__, écriture autour de 16 ns
  • Autres opérations : lecture via @property 19 ns, getattr() 13,8 ns, hasattr() 23,8 ns
  • Avec __slots__, le gain mémoire dépasse 2x, tandis que la vitesse d’accès reste comparable

JSON et sérialisation (JSON and Serialization)

  • Performances des bibliothèques alternatives par rapport à la bibliothèque standard
    • orjson sérialise un objet complexe en 310 ns, soit plus de 8 fois plus vite que json à 2,65 μs
    • msgspec atteint 445 ns, ujson 1,64 μs
  • En désérialisation aussi, orjson est le plus rapide avec 839 ns
  • Pydantic : model_dump_json() 1,54 μs, model_validate_json() 2,99 μs

Frameworks web (Web Frameworks)

  • À réponse JSON identique : FastAPI 8,63 μs, Starlette 8,01 μs, Litestar 8,19 μs, Flask 16,5 μs, Django 18,1 μs
  • FastAPI répond environ 2 fois plus vite que Django

Entrées/sorties fichier (File I/O)

  • Ouverture/fermeture d’un fichier : 9,05 μs, lecture de 1 KB : 10 μs, lecture de 1 MB : 33,6 μs
  • Écriture : 1 KB 35,1 μs, 1 MB 207 μs
  • Pickle est environ 2 fois plus rapide que json en sérialisation comme en désérialisation (pickle.dumps() 1,3 μs, json.dumps() 2,72 μs)

Base de données et persistance (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 est le plus rapide en lecture, tandis que diskcache excelle en écriture

Appels de fonction et exceptions (Function and Call Overhead)

  • Appels de fonction : fonction vide 22,4 ns, méthode 23,3 ns, lambda 19,7 ns
  • Gestion des exceptions : try/except (cas normal) 21,5 ns, exception levée 139 ns
  • Vérification de type : isinstance() 18,3 ns, comparaison type() 21,8 ns

Surcoût de l’asynchrone (Async Overhead)

  • Création de coroutine 47 ns, run_until_complete 27,6 μs
  • asyncio.sleep(0) 39,4 μs, gather(10 coroutines) 55 μs
  • Par rapport à un appel de fonction synchrone (20 ns), une exécution asynchrone (28 μs) est environ 1 000 fois plus lente

Enseignements clés (Key Takeaways)

  • La surcharge mémoire des objets Python est importante, même une liste vide consomme 56 octets
  • Les recherches dans les dictionnaires et sets sont des centaines de fois plus rapides qu’un parcours de liste
  • Les bibliothèques JSON alternatives comme orjson et msgspec sont 3 à 8 fois plus rapides que la solution standard
  • Le traitement asynchrone a un surcoût élevé et n’est recommandé que lorsqu’une exécution concurrente est nécessaire
  • __slots__ permet de réduire la mémoire de plus de moitié avec une perte de performance quasi nulle

1 commentaires

 
GN⁺ 2026-01-02
Réactions sur Hacker News
  • Beaucoup disent que « si l’on doit se soucier des chiffres de latence en Python, il faut utiliser un autre langage », mais je ne suis pas d’accord.
    Même de très grosses bases de code comme Instagram, Dropbox ou OpenAI ont grandi avec Python. On finit forcément par rencontrer des problèmes de performance, et l’important est d’être capable de les résoudre dans l’écosystème Python sans tout migrer vers un autre langage.
    La plupart des problèmes de performance ne viennent pas des limites du langage, mais d’un code inefficace. Par exemple, une boucle qui répète inutilement 10 000 appels de fonction.
    Mon quiz sur la latence en Python peut aussi valoir le détour.

    • Je m’occupe de l’optimisation des performances de systèmes écrits en Python. Mais ces chiffres n’ont pas vraiment de sens tant qu’ils ne deviennent pas un problème concret. Quand il y a un souci, je mesure directement. Si l’on écrit du code en essayant d’économiser des appels de méthode, on perd les avantages de Python.
    • Python est lent même pour des opérations de base. Il est lent sur des tâches simples comme les appels de fonction ou l’accès à un dictionnaire. En réalité, si Python a survécu, c’est grâce aux bibliothèques basées sur C/C++ comme Numpy.
    • Ces chiffres ne sont pas un problème propre à Python. En Zig aussi, on tient compte des cycles CPU ou des cache misses. Tous les langages ont une latence propre à certaines opérations. Il peut y avoir de bonnes raisons de ne pas utiliser Python, mais ce n’en est pas une.
    • Certaines opérations s’améliorent avec des modules de remplacement. Avoir ce genre de connaissances est important, mais ceux qui en ont vraiment besoin le savent probablement déjà. Cela dit, Python reste un excellent langage pour le prototypage.
    • Notre système de build est lui aussi en Python, donc j’aimerais conserver Python tout en améliorant les performances. C’est pourquoi ces chiffres sont très importants.
  • Paradoxalement, à partir du moment où ce genre de chiffres devient important, Python n’est plus l’outil adapté à ce travail.

    • Une approche réaliste consiste à conserver le code Python tout en déplaçant les parties critiques pour les performances vers des extensions C ou Rust. C’est ainsi que fonctionnent numpy, pandas ou PyTorch.
      En pratique, l’important est d’instrumenter le code et de trouver les goulots d’étranglement avec des outils comme pyspy. Si l’on commence à s’inquiéter de la vitesse d’ajout d’un élément dans une liste, c’est que cette opération ne devrait pas être faite en Python.
    • Je travaille avec Python depuis 20 ans, mais je n’ai jamais eu besoin de connaître ce genre de chiffres. À la place, je règle les problèmes avec des outils de profiling et avec Cython, SWIG ou le JIT.
    • Si une application est à un point où ces chiffres comptent vraiment, alors Python est trop haut niveau pour être facile à optimiser.
    • Pourtant, j’ai construit de grands pipelines de données en Python. La combinaison turbodbc + pandas donne des performances comparables au C++. Cela consomme plus de mémoire, mais si l’on tient compte du coût humain, c’est bien plus efficace.
      Cette approche est possible grâce à l’interopérabilité entre Python et C. Zig progresse aussi de plus en plus. Je ne piloterais pas un avion en Python, mais garder le sens des ressources reste important.
    • Ce genre de chiffres est un dernier recours. Il ne faut les considérer qu’après avoir réglé les goulots d’étranglement habituels comme les I/O disque, le réseau ou la complexité algorithmique.
  • Savoir combien d’octets occupe une chaîne vide n’a pas beaucoup d’intérêt. L’important, c’est de comprendre la complexité en temps et en espace.
    Plus que de savoir qu’un int fait 28 octets, il faut être capable de juger si un programme respecte ses contraintes de performance et, sinon, de trouver un meilleur algorithme.

    • Mais la performance est toujours une abstraction qui fuit. Qu’on en soit conscient ou non, elle influence tout le code.
      Par exemple, le fait que la concaténation de chaînes soit en O(n²) influence aussi la conception des f-strings en Python.
      Le fait que les dictionnaires soient rapides explique aussi leur usage généralisé dans tout Python.
      Ce type de chiffres sert à justifier par des mesures cette connaissance implicite.
    • Le fait qu’un int occupe 28 octets devient réellement important dans les problèmes où il faut créer un très grand nombre d’objets.
      Cela me rappelle cet article sur les difficultés rencontrées par Eric Raymond lors de la migration de GCC avec Reposurgeon.
  • Le titre prête à confusion, mais c’est en fait une parodie du célèbre texte de Jeff Dean de 2012, « Latency Numbers Every Programmer Should Know ».
    Ce genre de jeu sur les titres est courant dans les articles de CS.

    • Un article intitulé « latency numbers considered harmful is all you need » ferait probablement un carton dans le milieu académique.
    • Mais l’auteur de ce billet semble l’avoir écrit sérieusement. Ce n’est donc pas que les lecteurs aient mal compris le titre.
    • Pour que le titre tienne la route, il faudrait que les chiffres soient réellement utiles, or ils sont trop nombreux et peu concrets.
    • À noter que le texte original de Jeff Dean semble dater de bien avant 2012.
      C’était un document interne destiné à la conception du moteur de recherche des débuts de Google, notamment pour arbitrer entre RAM et disque.
      Plus tard, l’arrivée de la mémoire flash a fait évoluer ces chiffres, et il existe aussi une anecdote selon laquelle Jeff aurait conçu un algorithme de compression pour servir directement des données génomiques depuis de la mémoire flash.
  • La plupart des développeurs Python devraient se concentrer sur des sujets plus importants que ces détails de performance bas niveau.
    Ce type de ressource est utile comme référence, mais rarement nécessaire en pratique.

    • Mais avoir une culture générale sur les outils que l’on utilise a toujours de la valeur. C’est un capital intellectuel, et dans certaines situations cela peut beaucoup aider.
    • Quand on atteint les limites, il suffit de chercher un module implémenté en C, ou d’en écrire un soi-même. C’est ainsi que Python a évolué dès l’origine.
    • Moi aussi, dans la plupart des cas, j’ai travaillé avec l’intuition que c’était « assez rapide ». Cette ressource me permet simplement de mettre des chiffres sur cette intuition.
  • L’explication sur la taille des chaînes est erronée. Python possède en fait trois types de chaînes utilisant 1, 2 ou 4 octets par caractère.
    Voir ce blog pour plus de détails.

  • Le titre et les exemples de l’article sont un peu imprécis.
    Par exemple, dire que « item in set est 200 fois plus rapide que item in list » parle de test d’appartenance, pas de vitesse d’itération.
    Malgré cela, l’ensemble de la forme et de la structure est séduisant.

  • Il manque une mesure du temps de création des instances de classe.
    Après un refactoring, j’ai remplacé une simple structure en liste par des classes, et le temps d’exécution est passé de quelques microsecondes à plusieurs secondes.
    J’aurais aimé voir ce cas mesuré.

    • Cela me rappelle la blague du médecin : « Quand je fais ça, j’ai mal » — « Alors ne le faites pas ».
      Le problème peut venir d’un abus des classes. Parfois, une simple structure en liste convient mieux.
    • La création d’instances de classe n’est en général pas un problème de performance en soi.
      Il est plus probable qu’il y ait eu un mauvais usage de l’orienté objet.
      Mieux vaudrait publier le code sur StackOverflow ou CodeReview.SE pour obtenir des retours.
  • J’ai trouvé ce billet intéressant sous l’angle de la question : « y a-t-il quelque chose de fondamentalement cassé dans le Python moderne ? »
    En revanche, je ne suis pas d’accord avec l’idée qu’il faudrait « tous connaître » ces chiffres.
    Il suffit d’avoir une intuition correcte sur quelques opérations clés.

  • La plage de mise en cache des small int en Python n’est pas de 0 à 256, mais de -5 à 256.
    À cause de cela, les débutants confondent souvent l’identité (is) et l’égalité (==).

    • Java a un comportement similaire. Cela peut être déroutant pour les débutants.