2 Punkte von GN⁺ 2024-11-30 | 1 Kommentare | Auf WhatsApp teilen

Benchmark

  • Was sind Coroutinen?

    • Coroutinen sind Komponenten von Computerprogrammen, die die Ausführung eines Programms anhalten und wieder aufnehmen können; sie sind eine Verallgemeinerung von Subroutinen für kooperatives Multitasking.
    • Sie eignen sich zur Implementierung von Programmkomponenten wie kooperativen Aufgaben, Ausnahmen, Event-Loops, Iteratoren, unendlichen Listen und Pipes.
  • Rust

    • Es wurden zwei Programme geschrieben: eines mit tokio und eines mit async_std.
    • Beide sind in Rust gängige asynchrone Runtimes.
  • C#

    • C# unterstützt ähnlich wie Rust async/await.
    • Seit .NET 7 wird NativeAOT-Kompilierung bereitgestellt, wodurch verwalteter Code auch ohne VM ausgeführt werden kann.
  • NodeJS

    • Für asynchrone Aufgaben wird Promise.all verwendet.
  • Python

    • Für asynchrone Aufgaben wird das Modul asyncio verwendet.
  • Go

    • Nebenläufigkeit wird mit Goroutinen umgesetzt; mit WaitGroup wird auf den Abschluss der Aufgaben gewartet.
  • Java

    • Seit JDK 21 gibt es virtuelle Threads, ein Konzept ähnlich zu Goroutinen.
    • Mit GraalVM lassen sich native Images erzeugen.

Testumgebung

  • Hardware: 13th Gen Intel(R) Core(TM) i7-13700K
  • Betriebssystem: 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

Ergebnisse

  • Minimaler Speicherverbrauch

    • Rust, C# (NativeAOT) und Go werden zu nativen Binärdateien kompiliert und benötigen daher wenig Speicher.
    • Java (GraalVM Native Image) zeigte ebenfalls gute Ergebnisse, verbrauchte aber mehr Speicher als andere statisch kompilierte Sprachen.
  • 10K Aufgaben

    • Bei Rust steigt der Speicherverbrauch fast nicht an.
    • Auch C# (NativeAOT) verbraucht wenig Speicher.
    • Go verbraucht mehr Speicher als erwartet.
  • 100K Aufgaben

    • Rust und C# zeigen gute Ergebnisse.
    • C# (NativeAOT) verbraucht weniger Speicher als Rust.
  • 1 Million Aufgaben

    • C# übertrifft alle anderen Sprachen deutlich und verbraucht am wenigsten Speicher.
    • Auch Rust ist sehr speichereffizient.
    • Go verbraucht im Vergleich zu den anderen Sprachen viel Speicher.

Fazit

  • Viele gleichzeitige Aufgaben können erheblich Speicher verbrauchen, auch wenn sie keine komplexe Arbeit ausführen.
  • Die Verbesserungen bei .NET und NativeAOT sind auffällig, und auch mit GraalVM gebaute native Java-Images sind sehr speichereffizient.
  • Goroutinen sind beim Ressourcenverbrauch weiterhin ineffizient.

Anhang

  • In Rust (tokio) wurde statt join_all eine for-Schleife verwendet, wodurch sich der Speicherverbrauch halbierte. Damit ist Rust in diesem Benchmark der klare Spitzenreiter.

1 Kommentare

 
GN⁺ 2024-11-30
Hacker-News-Kommentare
  • Der Benchmark bildet die Unterschiede zwischen den asynchronen Verarbeitungsmodellen von Node und Go nicht angemessen ab. Node verwendet Promise.all, während Go Goroutinen nutzt, wodurch Unterschiede entstehen. Ein Vergleich des Speicherverbrauchs von asynchronem I/O und CPU-gebundenen Aufgaben wäre interessant

  • Es wird der Unterschied zwischen einer „10 Sekunden wartenden Aufgabe“ und einer „Aufgabe, die nach 10 Sekunden aufgeweckt wird“ erklärt. Der Speicherverbrauch des Go-Codes weicht im Vergleich zu den anderen Codes stark ab

  • Für einen fairen Vergleich zwischen Go und Node wird vorgeschlagen, Goroutinen zum Planen der Timer und Goroutinen zur Verarbeitung der Timersignale zu verwenden. Außerdem wird angemerkt, dass es seltsam ist, dass Bun und Deno bei Node nicht berücksichtigt wurden

  • Viele gleichzeitige Aufgaben können viel Speicher verbrauchen, aber wenn die Daten pro Aufgabe einige KB oder mehr betragen, ist der Speicher-Overhead des Schedulers vernachlässigbar

  • Je nach Definition von „gleichzeitigen Aufgaben“ kann sich der Speicherverbrauch unterscheiden. Bei einer effizienten Implementierung werden für 1 Mio. gleichzeitige Aufgaben etwa 200 MB benötigt

  • Es wird darauf hingewiesen, dass Go beim Speicherverbrauch gegenüber Java um mehr als das Doppelte zurückliegt, und dass der Benchmark keine realen Programme repräsentiert

  • Der Vergleich von Sprachen mit einfachem Code kann für Entwickler unfair sein; es wird empfohlen, reale Arbeit hinzuzufügen, um Unterschiede bei Speicherverbrauch und Scheduling zu messen

  • Es wird gesagt, dass Benchmarks oft voller Fehler sind und man nicht versteht, welche Motivation Menschen haben, solche Benchmarks zu veröffentlichen

  • Der Java-Benchmark ist möglicherweise fehlerhaft, da für ArrayList keine Anfangsgröße festgelegt wurde und dadurch viele unnötige Objekte erzeugt wurden

  • Es wird erklärt, warum der asynchrone Rust-Code schneller als erwartet abgeschlossen wird: tokio::time::sleep() verfolgt den Zeitpunkt, zu dem das Future erzeugt wurde