Les principaux chiffres de performance que tout programmeur Python devrait connaître
(mkennedy.codes)- 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
- Pour 1 000 instances : classe classique 165,2 KB, classe
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
@property19 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
orjsonsérialise un objet complexe en 310 ns, soit plus de 8 fois plus vite quejsonà 2,65 μsmsgspecatteint 445 ns,ujson1,64 μs
- En désérialisation aussi,
orjsonest 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
jsonen 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, comparaisontype()21,8 ns
Surcoût de l’asynchrone (Async Overhead)
- Création de coroutine 47 ns,
run_until_complete27,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
orjsonetmsgspecsont 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
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.
Paradoxalement, à partir du moment où ce genre de chiffres devient important, Python n’est plus l’outil adapté à ce travail.
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.
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.
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
intfait 28 octets, il faut être capable de juger si un programme respecte ses contraintes de performance et, sinon, de trouver un meilleur algorithme.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.
intoccupe 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.
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.
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 setest 200 fois plus rapide queitem 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é.
Le problème peut venir d’un abus des classes. Parfois, une simple structure en liste convient mieux.
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é (==).