2 Punkte von GN⁺ 2024-11-30 | Noch keine Kommentare. | Auf WhatsApp teilen
  • Ein Benchmark, der auf Basis aktueller Sprachen und Runtimes Ende 2024 den Speicherverbrauch von 1 bis 1 Million gleichzeitigen Tasks vergleicht; für die neuesten Ergebnisse wird auf eine separate Take-2-Seite verwiesen
  • Alle Tests folgen derselben Struktur: Jeder Task wartet 10 Sekunden, danach wird auf den Abschluss aller Tasks gewartet. Verglichen werden die Speichereigenschaften von Coroutinen, asynchronen Tasks, Goroutinen und virtuellen Threads statt vieler Threads
  • Verglichen werden Rust tokio und async_std, C# und NativeAOT, NodeJS, Python asyncio, Go-Goroutinen, Java virtual threads sowie Java GraalVM native image; der gesamte Code ist auf GitHub veröffentlicht
  • Mit steigender Task-Anzahl unterschieden sich die Speicherzuwächse je nach Runtime deutlich; bei 1 Million Tasks zeigte C# den niedrigsten Speicherverbrauch, während auch Rust effiziente Ergebnisse beibehielt
  • Das aktuelle .NET zeigte große Verbesserungen, und NativeAOT konkurrierte mit Rust; Go-Goroutinen verbrauchten bei 1 Million Tasks jedoch mehr als das 13-Fache des Siegerwerts und mehr als doppelt so viel Speicher wie Java

Benchmark-Methode und veröffentlichte Materialien

  • Es handelt sich um eine erneute Durchführung des Vergleichs zum Speicherverbrauch asynchroner Programmierung von 2023, mit den Ende 2024 aktuellen Sprachversionen
  • Oben steht der Hinweis, dass die neuesten Ergebnisse unter How Much Memory Do You Need in 2024 to Run 1 Million Concurrent Tasks? - Take 2 zu finden sind
  • Das Testprogramm erstellt N gleichzeitige Tasks anhand eines Kommandozeilenarguments; jeder Task wartet 10 Sekunden, und das Programm beendet sich, nachdem alle Tasks abgeschlossen sind
  • Der Fokus des Vergleichs liegt nicht auf mehreren Threads, sondern auf Coroutine-basierten Nebenläufigkeitsmodellen
  • Der vollständige Benchmark-Code ist unter async-runtimes-benchmarks-2024 veröffentlicht

Vergleichte Sprachen und Runtimes

  • Rust wird mit zwei asynchronen Runtimes verglichen: tokio und async_std
    • Beide sind in Rust weit verbreitete asynchrone Runtimes
  • C# unterstützt async/await direkt und führt Tasks mit Task.Delay und Task.WhenAll aus
    • Die seit .NET 7 verfügbare NativeAOT wird ebenfalls verglichen
    • NativeAOT kompiliert verwalteten Code direkt zu einem finalen Binary, das ohne VM ausgeführt werden kann
  • NodeJS kapselt setTimeout mit util.promisify und wartet anschließend mit Promise.all
  • Python verwendet asyncio.sleep und asyncio.gather
  • Go nutzt goroutine als Nebenläufigkeitsbaustein und wartet statt einzelner Awaits mit WaitGroup auf den Abschluss aller Tasks
  • Java verwendet die seit JDK 21 verfügbaren virtual threads
    • Auch GraalVMs native image wird verglichen
    • GraalVM native image ist als ähnliches Konzept zu .NET NativeAOT enthalten

Testumgebung

  • Hardware: 13th Gen Intel Core 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 build 23.0.1+11-39
  • Java (GraalVM): java 23.0.1 build 23.0.1+11-jvmci-b01
  • NodeJS: v23.2.0
  • Python: 3.13.0
  • Soweit möglich wurden alle Programme im release mode ausgeführt
  • Da in der Testumgebung libicu fehlte, wurden Internationalisierungs- und Globalisierungsunterstützung deaktiviert

Speicherveränderungen bei steigender Task-Anzahl

  • Minimaler Footprint: 1 Task

    • Um den von der Runtime selbst benötigten Speicher zu betrachten, wurde zunächst nur 1 Task ausgeführt
    • Rust, C# NativeAOT und Go wurden statisch zu nativen Binaries kompiliert, nutzten sehr wenig Speicher und zeigten ähnliche Ergebnisse
    • Auch Java GraalVM native image lieferte gute Ergebnisse, verbrauchte aber etwas mehr Speicher als die anderen statisch kompilierten Kandidaten
    • Programme, die auf einer verwalteten Plattform oder einem Interpreter laufen, verbrauchten mehr Speicher
    • In diesem Bereich zeigte Go den kleinsten Footprint
    • Java GraalVM nutzte deutlich mehr Speicher als OpenJDK Java; dies könnte möglicherweise per Konfiguration anpassbar sein
  • 10.000 Tasks

    • Die beiden Rust-Benchmarks erhöhten ihren Speicherverbrauch auch bei 10.000 Tasks kaum gegenüber dem minimalen Footprint und blieben sehr sparsam
    • C# NativeAOT folgte Rust dicht und verbrauchte nur etwa 10 MB Speicher
    • Der Speicherverbrauch von Go stieg in diesem Bereich stark an
    • Die virtual threads von Java GraalVM native image wirkten leichter als Go-Goroutinen
    • Go und Java GraalVM native image wurden zwar statisch zu nativen Binaries kompiliert, verbrauchten aber mehr RAM als C#, das auf einer VM läuft
  • 100.000 Tasks

    • Als die Task-Anzahl auf 100.000 stieg, begann der Speicherverbrauch aller Sprachen deutlich zuzunehmen
    • Rust und C# erzielten auch in diesem Bereich gute Ergebnisse
    • C# NativeAOT verbrauchte weniger RAM als Rust und lag vor allen Sprachen
    • Das Go-Programm fiel an diesem Punkt nicht nur hinter Rust, sondern auch hinter Java, C# und NodeJS zurück
    • Als Ausnahme ist Java unter GraalVM von den Kandidaten ausgenommen, die Go schlugen
  • 1 Million Tasks

    • Bei 1 Million Tasks lag C# klar vor allen anderen Sprachen
    • Rust setzte erwartungsgemäß seine guten Ergebnisse bei der Speichereffizienz fort
    • Der Abstand zwischen Go und den anderen Runtimes wurde größer
    • Go verbrauchte mehr als das 13-Fache des Siegerwerts an Speicher
    • Selbst im Vergleich zu Java verbrauchte Go mehr als doppelt so viel Speicher, ein Ergebnis, das der verbreiteten Wahrnehmung widerspricht, JVMs seien speicherhungrig und Go sei leichtgewichtig

Abschließende Beobachtungen

  • Bei sehr vielen gleichzeitigen Tasks kann erheblicher Speicher verbraucht werden, selbst wenn jeder Task keine komplexen Berechnungen ausführt
  • Je nach Sprach-Runtime zeigen sich unterschiedliche Trade-offs
    • Bei einer kleinen Zahl von Tasks kann eine Runtime leichtgewichtig und effizient sein
    • Beim Skalieren auf Hunderttausende Tasks kann der Speicherzuwachs stark ausfallen
  • Nach aktuellen Compilern und Runtimes zeigt .NET große Verbesserungen
  • .NET NativeAOT liefert Ergebnisse, die mit Rust konkurrenzfähig sind
  • Auch Java GraalVM native image erzielt gute Ergebnisse bei der Speichereffizienz
  • Go-Goroutinen zeigen beim Ressourcenverbrauch weiterhin ineffiziente Ergebnisse

Noch keine Kommentare.

Noch keine Kommentare.