Wie viel Speicher man 2024 braucht, um 1 Million gleichzeitige Tasks auszuführen
(hez2010.github.io)- 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
tokioundasync_std, C# und NativeAOT, NodeJS, Pythonasyncio, 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
Ngleichzeitige 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:
tokioundasync_std- Beide sind in Rust weit verbreitete asynchrone Runtimes
- C# unterstützt
async/awaitdirekt und führt Tasks mitTask.DelayundTask.WhenAllaus- 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
setTimeoutmitutil.promisifyund wartet anschließend mitPromise.all - Python verwendet
asyncio.sleepundasyncio.gather - Go nutzt goroutine als Nebenläufigkeitsbaustein und wartet statt einzelner Awaits mit
WaitGroupauf 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
libicufehlte, 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.