Wie viel Speicher für 1 Million gleichzeitige Aufgaben im Jahr 2024 benötigt wird
(hez2010.github.io)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
tokiound eines mitasync_std. - Beide sind in Rust gängige asynchrone Runtimes.
- Es wurden zwei Programme geschrieben: eines mit
-
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.
- C# unterstützt ähnlich wie Rust
-
NodeJS
- Für asynchrone Aufgaben wird
Promise.allverwendet.
- Für asynchrone Aufgaben wird
-
Python
- Für asynchrone Aufgaben wird das Modul
asyncioverwendet.
- Für asynchrone Aufgaben wird das Modul
-
Go
- Nebenläufigkeit wird mit Goroutinen umgesetzt; mit
WaitGroupwird auf den Abschluss der Aufgaben gewartet.
- Nebenläufigkeit wird mit Goroutinen umgesetzt; mit
-
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 stattjoin_alleinefor-Schleife verwendet, wodurch sich der Speicherverbrauch halbierte. Damit ist Rust in diesem Benchmark der klare Spitzenreiter.
1 Kommentare
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 interessantEs 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
ArrayListkeine Anfangsgröße festgelegt wurde und dadurch viele unnötige Objekte erzeugt wurdenEs 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