2 points par GN⁺ 2024-11-30 | 1 commentaires | Partager sur WhatsApp

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 tokio et async_std.
    • Les deux sont des runtimes asynchrones couramment utilisés en Rust.
  • 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.
  • NodeJS

    • Promise.all est utilisé pour les tâches asynchrones.
  • Python

    • Le module asyncio est utilisé pour exécuter les tâches asynchrones.
  • Go

    • La concurrence est implémentée avec des goroutines, et WaitGroup est utilisé pour attendre la fin des tâches.
  • 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 boucle for à la place de join_all a permis de réduire de moitié l’utilisation mémoire. Rust s’impose ainsi comme le leader absolu de ce benchmark.

1 commentaires

 
GN⁺ 2024-11-30
Commentaires sur Hacker News
  • Le benchmark ne reflète pas correctement les différences de gestion asynchrone entre Node et Go. Node utilise Promise.all tandis 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-bound

  • Explication 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 ArrayList n’a pas été définie, ce qui a créé beaucoup d’objets inutiles

  • Explication 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éé