Memoria necesaria para ejecutar 1 millón de tareas concurrentes en 2024
(hez2010.github.io)Benchmark
-
¿Qué es una corrutina?
- Una corrutina es un componente de un programa de computadora que puede pausar y reanudar la ejecución del programa, una generalización de las subrutinas para multitarea cooperativa.
- Es adecuada para implementar componentes de programa como tareas cooperativas, excepciones, event loops, iteradores, listas infinitas y pipes.
-
Rust
- Se escribieron dos programas: uno usando
tokioy otro usandoasync_std. - Ambos son runtimes asíncronos de uso común en Rust.
- Se escribieron dos programas: uno usando
-
C#
- C# soporta
async/await, de forma similar a Rust. - Desde .NET 7 ofrece compilación NativeAOT, lo que permite ejecutar código administrado sin una VM.
- C# soporta
-
NodeJS
- Usa
Promise.allpara tareas asíncronas.
- Usa
-
Python
- Usa el módulo
asynciopara realizar tareas asíncronas.
- Usa el módulo
-
Go
- Implementa concurrencia con goroutines y usa
WaitGrouppara esperar las tareas.
- Implementa concurrencia con goroutines y usa
-
Java
- Desde JDK 21 ofrece hilos virtuales, un concepto similar a las goroutines.
- Se pueden generar imágenes nativas con GraalVM.
Entorno de prueba
- Hardware: 13th Gen Intel(R) Core(TM) i7-13700K
- Sistema operativo: 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
Resultados
-
Uso mínimo de memoria
- Rust, C# (NativeAOT) y Go se compilan como binarios nativos, por lo que usan poca memoria.
- Java (imagen nativa de GraalVM) también mostró buen rendimiento, aunque usó más memoria que otros lenguajes compilados estáticamente.
-
10K tareas
- En Rust, el uso de memoria casi no aumenta.
- C# (NativeAOT) también usa poca memoria.
- Go usa más memoria de lo esperado.
-
100K tareas
- Rust y C# muestran buen rendimiento.
- C# (NativeAOT) usa menos memoria que Rust.
-
1 millón de tareas
- C# supera claramente a todos los lenguajes y usa la menor cantidad de memoria.
- Rust también destaca por su eficiencia de memoria.
- Go usa más memoria en comparación con otros lenguajes.
Conclusión
- Una gran cantidad de tareas concurrentes puede consumir una cantidad considerable de memoria, incluso si no realizan trabajo complejo.
- Las mejoras en .NET y NativeAOT llaman la atención, y la imagen nativa de Java construida con GraalVM también muestra una gran eficiencia de memoria.
- Las goroutines siguen siendo ineficientes en términos de consumo de recursos.
Apéndice
- En Rust (
tokio), usar un bucleforen lugar dejoin_allredujo el uso de memoria a la mitad. Rust fue el líder absoluto en este benchmark.
1 comentarios
Comentarios de Hacker News
El benchmark no refleja correctamente las diferencias en la forma en que Node y Go manejan el procesamiento asíncrono. Node usa
Promise.ally Go usa goroutines, por lo que hay diferencias. Sería interesante comparar la diferencia en uso de memoria entre I/O asíncrono y tareas limitadas por CPUSe explica la diferencia entre una “tarea que espera durante 10 segundos” y una “tarea que se despierta después de 10 segundos”. El uso de memoria del código en Go es muy distinto en comparación con los otros códigos
Se propone, para una comparación justa entre Go y Node, usar goroutines que programen temporizadores y goroutines que procesen las señales de esos temporizadores. Se menciona que es raro que Node no incluya a Bun y Deno
Muchas tareas concurrentes pueden consumir mucha memoria, pero si los datos por tarea son de varios KB o más, la sobrecarga de memoria del scheduler se vuelve prácticamente despreciable
El uso de memoria puede variar según cómo se defina “tareas concurrentes”. En una implementación eficiente, 1M tareas concurrentes requieren alrededor de 200MB
Se señala que Go queda más de 2 veces por detrás de Java en uso de memoria, y se menciona que el benchmark no representa programas reales
Comparar lenguajes con código simple puede ser injusto para los desarrolladores, y se recomienda agregar trabajo real para medir las diferencias de uso de memoria y de planificación
Se dice que los benchmarks suelen estar llenos de errores y que no se entienden las motivaciones de quienes publican este tipo de benchmarks
Es posible que el benchmark de Java esté mal, ya que no se especificó el tamaño inicial de
ArrayList, lo que genera muchos objetos innecesariosSe explica por qué el código asíncrono de Rust termina más rápido de lo esperado. Esto se debe a que
tokio::time::sleep()rastrea el momento en que se creó el futuro