- En situation de forte contention, les différences entre implémentations de mutex deviennent très visibles, et le
pthread_mutex_tde Cosmopolitan Libc affiche un temps d’exécution plus court et une utilisation CPU plus faible que les principales implémentations de Windows et Linux - Lors des tests sur un Threadripper 29070WX à 24 cœurs sous Windows, Cosmopolitan est 2,75 fois plus rapide que Microsoft SRWLOCK tout en utilisant 18 fois moins de ressources CPU
- Sur un Threadripper Pro 7995WX à 96 cœurs sous Linux, il est 3 fois plus rapide que glibc et 11 fois plus rapide que musl libc, avec un écart encore plus marqué sur le temps CPU
- Sur MacOS M2 Ultra, Apple Libc garde une légère avance, et Cosmopolitan utilise sur ARM un algorithme simple reposant sur l’appel système ulock de XNU
- La base de ces performances est l’intégration de nsync de Google, avec comme éléments clés le chemin rapide CAS, une file d’attente de waiters, futex/ulock/
WaitOnAddress(), la prévention de la famine et une conception à designated waker
Méthode de benchmark des mutex en contention
- Le test crée 30 threads, et chaque thread incrémente le même entier global
g_chores100,000 fois - Chaque incrément est exécuté dans une section critique très courte entre
pthread_mutex_lock()etpthread_mutex_unlock() - Les mesures sont exprimées en microsecondes et distinguent trois temps
- wall time : le temps réel d’exécution du programme, incluant l’overhead de création des threads et du join
- user time : le temps CPU consommé en espace utilisateur
- system time : le temps CPU consommé dans le noyau
- Comme plusieurs threads s’exécutent en parallèle, la somme de user time et system time peut dépasser le wall time
- En absence de contention, les écarts de performance entre implémentations sont généralement faibles, mais en situation de contention, les différences de conception des mutex deviennent très visibles
Windows : Cosmopolitan plus rapide que SRWLOCK
- Les tests Windows ont été réalisés sur un Threadripper 29070WX à 24 cœurs
- Le MutexShootout de Mark Waterman considérait SRWLOCK comme l’implémentation la plus solide de Windows en scénario de forte contention
- Dans les mêmes conditions, le
pthread_mutex_tde Cosmopolitan a enregistré un wall time plus court et une consommation CPU plus faible que SRWLOCK
| Implémentation | wall time | user time | system time |
|---|---|---|---|
Cosmopolitan pthread_mutex_t |
148,940µs | 328,125µs | 62,500µs |
| Microsoft SRWLOCK | 410,416µs | 5,515,625µs | 1,640,625µs |
Microsoft CRITICAL_SECTION |
949,187µs | 7,937,500µs | 5,078,125µs |
MSVC 2022 std::mutex |
991,750µs | 12,156,250µs | 4,031,250µs |
| spin lock | 1,165,435µs | 24,515,000µs | 15,000µs |
Cygwin pthread_mutex_t |
9,780,803µs | 1,937,000µs | 6,156,000µs |
- Le mutex Cosmopolitan est 2,75 fois plus rapide que Microsoft SRWLOCK et utilise 18 fois moins de ressources CPU
- Comparé au mutex Cygwin, qui fournit une implémentation POSIX sur Windows, il est 65 fois plus rapide
- Dans ce cas d’usage, le mutex Cygwin obtient même un résultat plus mauvais qu’un spin lock
Linux : un écart de temps CPU encore plus grand que le wall time
- Les tests Linux ont été réalisés sur un Threadripper Pro 7995WX à 96 cœurs
| Implémentation | wall time | user time | system time |
|---|---|---|---|
Cosmopolitan pthread_mutex_t |
36,905µs | 44,511µs | 23,492µs |
glibc pthread_mutex_t |
101,353µs | 150,706µs | 2,724,851µs |
| spin lock | 202,423µs | 4,694,749µs | 2,000µs |
Musl libc pthread_mutex_t |
411,013µs | 2,167,898µs | 9,926,850µs |
- Le mutex Cosmopolitan est 3 fois plus rapide que glibc et 11 fois plus rapide que musl libc
- En temps CPU, il consomme 42 fois moins que glibc et 178 fois moins que musl libc
- Sur une charge où tous les threads doivent effectuer un travail sérialisé, Cosmopolitan peut donner dans
htopl’impression qu’un seul cœur est actif - Dans la même situation, glibc et musl libc peuvent remplir fortement l’usage CPU, ce qui augmente la charge lorsqu’on exécute plusieurs tâches sur le même serveur
MacOS : Apple Libc garde une légère avance
- Les tests MacOS ont été réalisés sur M2 Ultra
| Implémentation | wall time | user time | system time |
|---|---|---|---|
| Apple Libc | 52,263µs | 43,202µs | 911,009µs |
Cosmopolitan pthread_mutex_t |
54,700µs | 63,055µs | 1,003,674µs |
- Sur MacOS M2 ARM64, Apple Libc est légèrement plus rapide que le mutex Cosmopolitan
- L’implémentation générale des mutex de Cosmopolitan ne fonctionne pas bien sur cette plateforme
- Sur MacOS ARM, Cosmopolitan utilise un algorithme plus simple fondé sur Futexes Are Tricky d’Ulrich Drepper
- Cette approche délègue l’essentiel du travail lourd à l’appel système ulock de XNU, et aboutit à des performances presque équivalentes à l’implémentation d’Apple
Base des performances : l’intégration de nsync
- Le cœur des performances du mutex Cosmopolitan est l’intégration de la bibliothèque nsync de Google
- nsync est une bibliothèque comptant 371 étoiles GitHub, écrite par Mike Burrows de Google
- Lors de l’intégration dans Cosmopolitan, les travaux suivants ont été réalisés
- correction d’un bug longtemps passé inaperçu dans la fonction d’unlock des mutex de nsync
- portage vers les opérations atomiques C11 sur AARCH64, rendant le mutex nsync en contention 30 % plus rapide que nsync upstream
- réécriture de l’intégration système de type futex pour permettre la portabilité du runtime
- adaptation pour fonctionner proprement avec l’annulation de threads POSIX
Fonctionnement de nsync
- nsync tente d’abord immédiatement un CAS (compare and swap) optimiste pour acquérir rapidement le lock
- Si le lock ne peut pas être acquis, le thread appelant est ajouté à une liste doublement chaînée de waiters
- chaque waiter possède son propre sémaphore sur une ligne de cache indépendante
- une fois mis en attente, le thread ne touche plus au lock principal
- c’est essentiel pour réduire l’overhead de communication causé par plusieurs cœurs qui touchent la même ligne de cache
- pour le contexte, voir What Every Programmer Should Know About Memory d’Ulrich Drepper
- nsync utilise le futex du système d’exploitation pour endormir les threads
- sur MacOS, futex s’appelle ulock
- sur Windows,
WaitOnAddress()joue le rôle de futex - parmi les OS pris en charge par Cosmo, seul NetBSD ne dispose pas de futex ; il implémente les sémaphores POSIX dans l’espace noyau et requiert un nouveau descripteur de fichier pour chaque sémaphore
- nsync évite la famine (starvation) grâce au concept de « long wait »
- si un waiter a été réveillé 30 fois mais échoue à chaque fois à acquérir le lock en interne, un bit est ajouté au lock pour empêcher qu’un thread n’ayant pas encore attendu l’obtienne
- quand ce bit est présent, le CAS initial des nouveaux threads échoue jusqu’à ce que la file d’attente se soit partiellement vidée
- Les cas de contention sur de petites sections critiques sont accélérés par le concept de designated waker
- lorsqu’un thread se réveille pour tenter d’acquérir le lock, un bit est positionné sur le lock principal
- dans nsync, la fonction d’unlock est chargée de réveiller le thread en attente suivant
- grâce à ce bit, le thread qui fait l’unlock n’a pas besoin de réveiller un second waiter si un thread réveillé est déjà en train de tenter l’acquisition
- Le code source associé se trouve dans
cosmopolitan/third_party/nsync/mu.cetcosmopolitan/libc/intrin/pthread_mutex_lock.c
Service réel et code de validation
- Une démo en production utilisant le mutex Cosmo est visible sur le serveur http://ipv4.games/
- Ce service tourne sur une VM GCE à 2 cœurs et a jusqu’ici résisté à un botnet DDoS atteignant 49,131,669 adresses IP
- Grâce à nsync, il a été possible de déplacer les requêtes SQL vers des threads en arrière-plan et d’utiliser une architecture où les threads s’échangent des messages
- Les indicateurs d’état sont consultables sur /statusz
- Le code de benchmark mesure le wall time avec
gettimeofday()et les user time / system time avecgetrusage() - À la fin, il vérifie que
g_chores == THREADS * ITERATIONSafin de confirmer que tous les incréments ont bien été exécutés
Précautions à propos des spin locks
- En absence de contention, les différences entre implémentations de mutex sont faibles, et un spin lock de quelques lignes peut parfois sembler meilleur
- Mais il ne faut utiliser un spin lock que lorsqu’il n’existe vraiment aucune autre option
- Il peut être utile dans des contextes comme le noyau, où des contraintes de très bas niveau empêchent l’usage de mécanismes plus complexes
- Un spin lock peut aussi être utilisé dans certains détails d’implémentation interne de nsync
- Si l’on ne regarde que le wall time, les performances d’un spin lock peuvent paraître bonnes ; il faut donc vérifier aussi le temps CPU avec
getrusage()
1 commentaires
Avis sur Hacker News
Les nouvelles implémentations de mutex et leurs comparaisons sont toujours intéressantes, mais je n’aime pas cette méthode de benchmark. Cela ressemble presque à un microbenchmark.
Les gens qui déploient des verrous rapides utilisent généralement de très gros programmes multithread comme principal moyen de tester les performances. Dans des charges de travail complexes où la longueur des sections critiques, le nombre de threads en concurrence et le degré de contention varient, les facteurs qui rendent un mutex rapide ou lent semblent changer.
Pour référence, j’ai écrit les verrous rapides de WebKit, inventé l’abstraction ParkingLot pour l’implémentation de verrous (également utilisée dans Rust et Unreal Engine), et j’ai autrefois mené des recherches et rédigé un article sur les verrous rapides pour Java.
En tant que programmeur audio temps réel, le coût d’acquisition d’un mutex qui n’est pas déjà verrouillé est plus important. Dans notre application, c’est de loin la situation la plus courante. De même, j’aimerais connaître le coût d’une opération
try-lockqui va échouer, plutôt que lorsque N threads sont en concurrence.Comme Cosmopolitan est open source, je pourrais le mesurer moi-même, mais c’est tout de même dommage.
Comme pour les tables de hachage, il est rare qu’une seule table de hachage soit meilleure pour toutes les charges de travail possibles.
Un mutex qui, en cas d’échec d’acquisition du verrou, dort pendant une durée fixe (par exemple 100 µs) aura presque toujours tendance à regrouper le travail, se rapprochant de ce comportement, et pourra « gagner » le benchmark. Mais dans une application réelle, dès qu’il y a un peu de contention, un tel mutex est affreux.
Je ne dis pas que ce mutex est mauvais ni que le mutex pthread est bon, mais que ce microbenchmark ne mesure rien qui permette de prédire les performances d’une application réelle.
À propos du passage disant que « la raison pour laquelle le mutex de Cosmopolitan est bon, c’est qu’il utilise une bibliothèque appelée nsync », je n’avais jamais entendu parler de nsync, mais Mike Burrows a aussi écrit l’implémentation de mutex de production de Google : https://github.com/abseil/abseil-cpp/blob/master/absl/synchr...
Je me demande donc pourquoi cette implémentation de mutex a été absente du benchmark. Et si, sur macOS, on délègue à
__ulock, il me semble qu’on pourrait obtenir cela plus simplement en utilisant seulement les fonctions membreswait()etnotify_one()de la bibliothèque atomic de libc++.Il y a aussi eu autrefois un gros thread sur l’amélioration de l’implémentation des mutex de Rust : https://github.com/rust-lang/rust/issues/93740#issuecomment-... Ce qui est intéressant, c’est que le fonctionnement interne de presque toutes les implémentations populaires de mutex y est discuté en détail.
C’est peut-être vrai, mais je ne peux pas le vérifier directement. C’était un ingénieur extrêmement intelligent, très attaché à l’efficacité. Cela dit, nous ne faisions pas tourner les choses très longtemps sur un même serveur.
L’implémentation actuelle des mutex de Rust a été intégrée plus tôt cette année ; sous Linux, elle n’est peut-être pas très différente, mais sous Windows et Mac, il me semble que c’est du nouveau travail.
Les explications de Mara sur les entrailles des autres implémentations restent malgré tout intéressantes, mais mieux vaut vérifier si l’information n’est pas obsolète pour son propre cas.
https://awards.acm.org/award-recipients/burrows_9434147
La phrase « c’est encore une nouvelle bibliothèque C, donc il y a des aspérités, mais elle s’améliore si vite que ne pas l’utiliser en production commence à ressembler à un manquement à la responsabilité professionnelle » est assez étrange. J’ai beaucoup d’estime pour le projet Cosmopolitan, mais ce genre d’affirmation de supériorité exagérée est généralement un très mauvais signal d’alerte.
Je comprends aussi que cela puisse paraître abrupt à certains. Il y a déjà eu ce genre de drama autour de llamacpp par le passé.
En production, les priorités absolues sont la stabilité, la prévisibilité et la fiabilité, pas le fait que « ça s’améliore incroyablement vite ». Bien sûr, les performances comptent aussi. Un code plus rapide peut réduire l’infrastructure, ce qui est bon pour les coûts et l’environnement. Mais la vitesse arrive en dernier.
Par exemple, APE me semble être un hack très impressionnant, mais on peut aussi formuler la critique suivante : « cela veut-il dire que ce n’est plus seulement instable sur une plateforme, mais potentiellement instable sur plusieurs plateformes en même temps ? »
Plus je passe de temps dans la tech, plus je réalise que les gains parfaitement mutuels sont extrêmement rares, et que la plupart des choses sont des compromis avec des avantages et des pertes.
C’est complètement hors sujet, mais en tant que développeur de jeux, j’en suis venu à apprécier les mutex lents qui font beaucoup de travail de debug dans tous les builds de développement. Ils ont un nom/ID de debug, suivent leur propriétaire, remontent au profiler le temps passé en contention, et signalent aussi les changements de propriétaire au profiler.
Les jeux ont tendance à organiser la concurrence différemment, et les patterns évitant les verrous ont aussi évolué. Mais ces patterns sont difficiles à utiliser et obligent les programmeurs à changer la structure. La plupart du code commence par : « mettons déjà un verrou ici et passons le milestone ».
Même un verrou rapide peut devenir lent de manière imprévisible, et casser toute garantie temps réel s’il y en avait une. Il peut être rapide en moyenne, mais la latence de queue ne disparaît pas. Je n’ai pas envie d’être celui qui revient enquêter sur « notre jeu saccade », mais en général c’est moi qui finis par l’être.
Je préfère donc utiliser des verrous lents. Des verrous qui apparaissent en gros rouge dans le profiler. Quand on voit qu’ils posent problème, on les refactorise pour les supprimer.
Je sais que c’est une exigence difficile. Dans une production AAA, on peut compter sur les doigts d’une main les personnes qui savent utiliser un profiler. Même après avoir vu plusieurs productions, c’était toujours le cas.
Désolé pour la plainte, mais j’espère que la recherche sur les primitives et algorithmes de concurrence rapides continuera.
Dans les jeux, on ne veut absolument jamais de contention de verrous si on peut l’éviter, et dans beaucoup de cas on peut prouver qu’il est inutile de prendre un verrou. Par exemple, chaque frame est divisée en phases, et l’accès mutable à une ressource partagée donnée n’est nécessaire que pendant une phase précise, comme
update()avantrender(), ou le hot reload des assets.Avec les scoped threads et les règles d’emprunt de Rust, on peut structurer le code de manière à ne pas avoir besoin de mutex du tout, avec la certitude que si le code change plus tard et qu’un mutex devient nécessaire, le compilateur produira une erreur stricte.
Quand c’est possible, je préfère toujours recevoir une erreur de compilation plutôt qu’un pic dans le profiler.
D’un côté, la lignée Cosmo/APE/redbean a vraiment l’air impressionnante, et les commentaires des articles à ce sujet sont globalement positifs, avec peu de remises en cause du concept lui-même. Mais d’un autre côté, on entend très rarement dire que d’autres personnes l’utilisent
Tout le monde ne partage pas largement son travail, mais après plusieurs années, on pourrait s’attendre à voir au moins quelques billets de rétrospective de projets. Toutes les mentions de Cosmo/APE/redbean que j’ai vues venaient du site de Justine
Du coup, je me pose la question. Y a-t-il un piège caché ? Est-ce un outil qui fait des choses douteuses pour obtenir ses résultats ? Est-ce une blague ou un troll à la tom7 que je ne comprends pas parce que je ne connais pas assez en profondeur les compilateurs ou les runtimes ? Ou bien est-ce vraiment une famille d’outils ingénieux qui n’est simplement pas encore largement répandue ?
La plupart des gens qui créent des logiciels multiplateformes ne veulent pas un unique exécutable qui tourne sur toutes les plateformes, mais une base de code unique qui fonctionne correctement sur chacune des plateformes prises en charge
De ce point de vue, les langages comme Go, où l’on peut cross-compiler pour toutes les cibles si l’on évite CGO, sont agréables. Mais la magie d’APE, exécutable de trois façons différentes, a beau être vraiment brillante, elle n’inspire pas confiance quant à sa pérennité, et dans la plupart des cas elle n’apporte pas grand-chose de concret
Chaque plateforme ayant ses propres exigences de packaging et de signature, il vaut mieux compiler séparément pour chaque cible de plateforme
Par exemple, si l’on peut déjà cross-compiler un projet pour d’autres systèmes d’exploitation et plateformes, ou si l’on dispose déjà de cette infrastructure de build, il n’y a aucune raison de chercher une solution produisant un binaire unique qui fonctionne partout
De plus, APE utilise des hacks ingénieux pour s’exécuter sur plusieurs systèmes d’exploitation. Et si ces hacks cassent un jour avec l’évolution des formats d’exécutables ? Et si personne n’a le temps d’adapter APE à ces changements ?
À l’inverse, les outils ennuyeux comme gcc, clang, go ou rust continueront à être mis à jour et à fonctionner sur des systèmes d’exploitation qui évoluent. C’est pourquoi je reste simplement du côté ennuyeux. Si je ne me soucie pas de ce qui est astucieux, c’est parce que ce qui est ennuyeux fonctionne tout simplement bien pour moi
On peut aussi l’exécuter sans poids intégrés et lui faire lire les poids depuis le système de fichiers. C’est peut-être la façon la plus simple de « télécharger et lancer directement » un LLM local
Mais comme technologie de base, à la manière de
libc, cela semble surtout utile pour des jouets amusants ou de petits projets personnelsDans ce contexte, je trouve un peu étrange de le présenter comme une alternative sérieuse à des choses comme
glibc,musloumsvcrt. C’est un hack très mignon, mais je serais assez déconcerté si je le découvrais dans quelque chose dont je dépends sérieusementDes modèles populaires reconditionnés dans ce format sont aussi régulièrement publiés sur Hugging Face : https://huggingface.co/models?search=llamafile
Reste à savoir si cela a une utilité pratique au-delà de tester rapidement de petits modèles
Si c’est si bien, je me demande pourquoi toutes les bibliothèques C n’ont pas adopté la même astuce.
À mon avis, il est fort possible que ces astuces ne soient systématiquement rapides que sur certaines architectures, certains modèles de CPU, certaines charges de travail ou certains schémas d’accès. Si l’on benchmarkait correctement diverses charges de travail sur tout le matériel pris en charge, on n’obtiendrait peut-être pas le même avantage.
Ou alors la sémantique de l’API pthread que Cosmopolitan essaie d’implémenter est subtilement différente, et cette implémentation ne respecte peut-être pas strictement la spécification.
J’ai du mal à imaginer que plusieurs auteurs de libc ne suivent pas les recherches récentes sur les primitives du système d’exploitation.
Le malloc de glibc est à peu près utilisable, mais il se fait facilement dépasser par des alternatives plus modernes en vitesse globale et en scalabilité. Il fragmente beaucoup et empire avec le temps, et il y a de nombreux réglages comme
MALLOC_ARENA_MAXqui ont un gros impact sur les charges de travail réelles. Le malloc de musl est catastrophique à tous les niveaux côté performances. Utiliser l’allocateur de musl dans un programme multithread dégradait tellement les performances qu’on aurait presque pu parler de négligence.musl n’a pas non plus de routines de comparaison de chaînes optimisées en SIMD, par exemple. Vous seriez surpris du nombre de cycles CPU consacrés à ce genre d’opérations dans des programmes non triviaux ; cela apparaît clairement dans les profils réels, et les améliorer bénéficie presque universellement à tous les programmes. Les routines optimisées de glibc sont bonnes, mais semblent encore pouvoir être accélérées.
Ce ne sont pas des « optimisations propres à une seule architecture et non généralisables ». Ces deux domaines en particulier sont bien explorés et bien compris : ils réduisent le temps réel d’un facteur 2 à 5 sur presque toutes les charges de travail, et améliorent aussi fortement l’utilisation de l’ensemble de travail à long terme. Alors pourquoi cela n’a-t-il pas été adopté ? Comme toujours, probablement parce qu’il y avait autre chose à faire, ou des priorités contradictoires, comme chez musl où la simplicité passe avant les performances maximales.
Je ne blâme pas ces projets. Personne ne dit : « mon programme est lamentablement lent, conçu pour ne rien faire correctement, et j’en suis fier ». Mais l’idée que les gens qui travaillent sur ces projets auraient toujours choisi uniquement des conceptions situées sur une frontière de Pareto parfaite n’est absolument pas réaliste, et ne reflète pas la manière dont la plupart des projets fonctionnent réellement.
Changer quelque chose dans glibc, ou son équivalent côté C++, prend une éternité.
Il existe plusieurs types de primitives de synchronisation, et pthreads n’en prend en charge qu’une partie. Si l’on se limite à cela, on obtient en général de la portabilité au prix des performances.
Je ne sais pas pour les mainteneurs de libc, mais en tant que mainteneur de quelques projets, je ne cherche pas à implémenter les dernières recherches. J’essaie de préserver la stabilité et de vérifier que les performances restent acceptables. Implémenter des travaux de recherche dépasse mon budget de « maintenance ».
Un homme et un statisticien marchent dans la rue et voient un billet de 50 euros. Le statisticien continue d’avancer, et l’homme s’arrête pour dire : « Regardez, il y a de l’argent par terre. » Le statisticien répond : « Ce doit être un faux. Si c’était un vrai, quelqu’un l’aurait déjà ramassé », puis il continue son chemin. L’autre homme ramasse le billet.
Les threads et les mutex font partie des éléments les plus complexifiants en informatique. Je reste toujours sceptique face à une nouvelle implémentation tant qu’elle n’a pas été utilisée à grande échelle pendant des années.
Les bugs dans ce type de mécanismes de threading échappent souvent même aux revues les plus poussées. Quand Java est arrivé au milieu des années 90, il a mis au jour toutes sortes de bugs de threads et de mutex dans Solaris.
Ce dont on a besoin, ce n’est pas de l’implémentation de mutex la plus rapide, mais d’une implémentation fiable.
Ce code ne benchmarke pas les performances de verrouillage d’un mutex, mais la contention sur mutex. Si vous utilisez des verrous de cette manière, vous devriez réévaluer votre code.
Chaque thread verrouille et déverrouille le mutex à chaque incrément de
g_chores. Cela crée un surcoût lié aux acquisitions et libérations fréquentes du mutex, répété 100 000 fois par thread.Ce surcoût masque les différences de performances réelles entre mécanismes de verrouillage, car le benchmark est dominé par la contention sur le verrou plutôt que par un travail réel. Ce genre de benchmark est inutile.
Je suis fan de Justine et de son travail, mais c’est probablement l’un des cas de test les moins intéressants pour benchmarker un mutex. Une situation où plusieurs threads martèlent constamment le même mutex devrait être évitée dès le départ.
Je ne trouve donc pas très intéressant de savoir quelle implémentation de mutex gère le mieux ce cas.
Il manque un point important : en situation de contention, un verrou peu performant peut avoir des effets systémiques très négatifs, comme créer un point chaud dans le réseau mémoire, et cela apparaîtrait aussi ici.
Je vois plusieurs cas où plusieurs threads convergent vers le même mutex. Un exemple simple est le remplissage simultané d’une structure de données comme une liste ou un dictionnaire.
On peut aussi le faire par passage de messages, mais cela peut consommer plus de mémoire et être plus lent que d’attendre pour écrire à un emplacement partagé.
La production n’est pas une question de vitesse, d’efficacité, ni de « hack malin » évident.
Si je dois sacrifier 50 % d’efficacité pour avoir la garantie de ne pas être appelé un dimanche à 3 h du matin pour réparer un système en panne, je ferai ce choix à chaque fois.
La production est une question de fiabilité, et écrire du code fiable est 10 fois plus difficile qu’écrire du code « rapide ».