Quantité de mémoire nécessaire pour exécuter 1 million de tâches concurrentes en 2024
(hez2010.github.io)Benchmark
-
Qu’est-ce qu’une coroutine ?
- Une coroutine est un composant de programme informatique capable de suspendre puis de reprendre son exécution, une généralisation des sous-routines pour le multitâche coopératif.
- Elle convient à l’implémentation de composants de programme comme les tâches coopératives, les exceptions, les boucles d’événements, les itérateurs, les listes infinies et les pipes.
-
Rust
- Deux programmes ont été écrits : des programmes utilisant
tokioetasync_std. - Les deux sont des runtimes asynchrones couramment utilisés en Rust.
- Deux programmes ont été écrits : des programmes utilisant
-
C#
- C# prend en charge
async/await, de façon similaire à Rust. - Depuis .NET 7, il propose la compilation NativeAOT, qui permet d’exécuter du code managé sans VM.
- C# prend en charge
-
NodeJS
Promise.allest utilisé pour les tâches asynchrones.
-
Python
- Le module
asyncioest utilisé pour exécuter les tâches asynchrones.
- Le module
-
Go
- La concurrence est implémentée avec des goroutines, et
WaitGroupest utilisé pour attendre la fin des tâches.
- La concurrence est implémentée avec des goroutines, et
-
Java
- Depuis le JDK 21, Java propose les threads virtuels, un concept similaire aux goroutines.
- GraalVM permet de générer des images natives.
Environnement de test
- Matériel : Intel(R) Core(TM) i7-13700K de 13e génération
- Système d’exploitation : Debian GNU/Linux 12 (bookworm)
- Rust : 1.82.0
- .NET : 9.0.100
- Go : 1.23.3
- Java : openjdk 23.0.1
- Java (GraalVM) : java 23.0.1
- NodeJS : v23.2.0
- Python : 3.13.0
Résultats
-
Utilisation mémoire minimale
- Rust, C# (NativeAOT) et Go sont compilés en binaires natifs et utilisent peu de mémoire.
- Java (image native GraalVM) a également montré de bonnes performances, tout en utilisant plus de mémoire que les autres langages compilés statiquement.
-
10K tâches
- En Rust, l’utilisation mémoire n’augmente presque pas.
- C# (NativeAOT) utilise également peu de mémoire.
- Go utilise plus de mémoire que prévu.
-
100K tâches
- Rust et C# affichent de bonnes performances.
- C# (NativeAOT) utilise moins de mémoire que Rust.
-
1 million de tâches
- C# surclasse tous les autres langages et utilise le moins de mémoire.
- Rust reste lui aussi très efficace sur le plan mémoire.
- Go consomme plus de mémoire que les autres langages.
Conclusion
- Un grand nombre de tâches concurrentes peut consommer une quantité importante de mémoire, même lorsqu’elles n’exécutent pas d’opérations complexes.
- Les progrès de .NET et de NativeAOT sont particulièrement visibles, et l’image native Java construite avec GraalVM montre également une excellente efficacité mémoire.
- Les goroutines restent inefficaces du point de vue de la consommation de ressources.
Annexe
- En Rust (
tokio), l’utilisation d’une boucleforà la place dejoin_alla permis de réduire de moitié l’utilisation mémoire. Rust s’impose ainsi comme le leader absolu de ce benchmark.
1 commentaires
Commentaires sur Hacker News
Le benchmark ne reflète pas correctement les différences de gestion asynchrone entre Node et Go. Node utilise
Promise.alltandis que Go utilise des goroutines, ce qui crée un écart. Il serait intéressant de comparer les différences d’utilisation mémoire entre les E/S asynchrones et les tâches CPU-boundExplication de la différence entre une « tâche qui attend pendant 10 secondes » et une « tâche qui se réveille après 10 secondes ». L’utilisation mémoire du code Go diffère fortement de celle des autres codes
Proposition, pour une comparaison équitable entre Go et Node, d’utiliser des goroutines qui planifient les timers et des goroutines qui traitent les signaux des timers. Il est aussi mentionné qu’il est étrange que Bun et Deno ne soient pas inclus pour Node
Un grand nombre de tâches concurrentes peut consommer beaucoup de mémoire, mais si les données par tâche dépassent quelques Ko, l’overhead mémoire de l’ordonnanceur devient négligeable
Selon la définition de « tâche concurrente », l’utilisation mémoire peut varier. Dans une implémentation efficace, 1M de tâches concurrentes nécessitent environ 200 Mo
Il est souligné que Go est à plus du double de Java en consommation mémoire, et que le benchmark ne représente pas un programme réel
Comparer des langages avec un code simple peut être injuste pour les développeurs, et il est recommandé d’ajouter une vraie charge de travail afin de mesurer les différences de consommation mémoire et d’ordonnancement
Les benchmarks sont souvent truffés d’erreurs, et l’auteur dit ne pas comprendre les motivations de ceux qui publient ce genre de benchmarks
Le benchmark Java est peut-être erroné, car la taille initiale de
ArrayListn’a pas été définie, ce qui a créé beaucoup d’objets inutilesExplication de la raison pour laquelle le code asynchrone en Rust se termine plus vite que prévu :
tokio::time::sleep()suit le moment où le future a été créé