2 puntos por GN⁺ 2024-11-30 | Aún no hay comentarios. | Compartir por WhatsApp
  • Es un benchmark que compara el uso de memoria de 1 a 1 millón de tareas concurrentes con base en los lenguajes y runtimes más recientes a finales de 2024, y se indica consultar una página separada de Take 2 para ver los resultados más recientes
  • Todas las pruebas se ajustaron a la misma estructura: cada tarea espera 10 segundos y luego se espera a que todas terminen; se comparan las características de memoria de corrutinas, tareas asíncronas, goroutines e hilos virtuales, más que de múltiples hilos
  • Los comparados son Rust tokio y async_std, C# y NativeAOT, NodeJS, Python asyncio, goroutine de Go, virtual thread de Java, y la native image de GraalVM para Java; todo el código está publicado en GitHub
  • A medida que aumentó la cantidad de tareas, la magnitud del aumento de memoria varió mucho entre runtimes, y con 1 millón de tareas C# mostró el menor uso de memoria, mientras Rust también mantuvo resultados eficientes
  • El .NET más reciente mostró una gran mejora y NativeAOT compitió con Rust, pero las goroutines de Go usaron más de 13 veces la memoria del ganador y más del doble que Java con 1 millón de tareas

Método del benchmark y materiales públicos

  • Este es el resultado de repetir la comparación del consumo de memoria en programación asíncrona de 2023 usando las versiones más recientes de cada lenguaje a finales de 2024
  • En la parte superior se indica que, para ver los resultados más recientes, hay que consultar How Much Memory Do You Need in 2024 to Run 1 Million Concurrent Tasks? - Take 2
  • El programa de prueba crea N tareas concurrentes recibidas como argumento de línea de comandos, cada una espera durante 10 segundos, y el programa termina cuando todas finalizan
  • El foco de la comparación no está en múltiples hilos, sino en modelos de concurrencia basados en corrutinas
  • Todo el código del benchmark está publicado en async-runtimes-benchmarks-2024

Lenguajes y runtimes comparados

  • Rust se compara usando dos runtimes asíncronos: tokio y async_std
    • Ambos son runtimes asíncronos ampliamente usados en Rust
  • C# tiene soporte directo para async/await y ejecuta las tareas con Task.Delay y Task.WhenAll
    • También se compara NativeAOT, disponible desde .NET 7
    • NativeAOT compila directamente el código administrado a un binario final para poder ejecutarlo sin VM
  • NodeJS envuelve setTimeout con util.promisify y luego espera con Promise.all
  • Python usa asyncio.sleep y asyncio.gather
  • Go usa goroutine como componente de concurrencia y, en lugar de await individual, espera a que todas las tareas terminen con WaitGroup
  • Java usa virtual thread, disponible desde JDK 21
    • También se compara la native image de GraalVM
    • La native image de GraalVM se incluye como un concepto similar a .NET NativeAOT

Entorno de prueba

  • Hardware: 13th Gen Intel Core 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 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
  • Siempre que fue posible, todos los programas se ejecutaron en release mode
  • Como el entorno de prueba no tenía libicu, el soporte de internacionalización y globalización quedó desactivado

Cambios de memoria al aumentar la cantidad de tareas

  • Huella mínima: 1 tarea

    • Para ver cuánta memoria requiere el runtime por sí mismo, primero se ejecutó solo 1 tarea
    • Rust, C# NativeAOT y Go se compilaron como binarios nativos estáticos y usaron muy poca memoria, con resultados similares entre sí
    • La native image de Java GraalVM también dio buenos resultados, aunque usó un poco más de memoria que los otros casos compilados estáticamente
    • Los programas que se ejecutan sobre plataformas administradas o intérpretes consumieron más memoria
    • En este tramo, Go mostró la huella más pequeña
    • Java GraalVM usó mucha más memoria que Java sobre OpenJDK, aunque esto podría ajustarse con configuración
  • 10 mil tareas

    • Los dos benchmarks de Rust no aumentaron mucho su uso de memoria frente a la huella mínima incluso con 10 mil tareas, y mantuvieron un consumo muy bajo
    • C# NativeAOT también usó solo alrededor de 10 MB de memoria y quedó muy cerca de Rust
    • El uso de memoria de Go aumentó bastante en este tramo
    • Las virtual threads de Java en GraalVM parecen más ligeras que las goroutines de Go
    • Go y la native image de Java GraalVM se compilaron como binarios nativos estáticos, pero aun así usaron más RAM que C#, que corre sobre una VM
  • 100 mil tareas

    • Cuando la cantidad de tareas aumentó a 100 mil, el consumo de memoria de todos los lenguajes empezó a crecer con fuerza
    • Rust y C# siguieron mostrando buenos resultados en este tramo
    • C# NativeAOT usó menos RAM que Rust y quedó por delante de todos los lenguajes
    • El programa de Go ya iba por detrás no solo de Rust, sino también de Java, C# y NodeJS
    • Como excepción, Java ejecutado en GraalVM no se incluye entre los que superaron a Go
  • 1 millón de tareas

    • Con 1 millón de tareas, C# quedó claramente por delante de todos los demás lenguajes
    • Rust, como era de esperarse, mantuvo buenos resultados en eficiencia de memoria
    • La brecha entre Go y los otros runtimes se hizo aún mayor
    • Go usó más de 13 veces la memoria del resultado ganador
    • Incluso frente a Java, Go usó más del doble de memoria, lo que contradice la percepción común de que la JVM consume mucha memoria y Go es liviano

Observaciones finales

  • Cuando la cantidad de tareas concurrentes es muy grande, pueden usar una cantidad considerable de memoria incluso si cada tarea no hace cálculos complejos
  • Los trade-offs varían según el runtime del lenguaje
    • Con pocas tareas puede ser ligero y eficiente
    • Pero al escalar a cientos de miles de tareas, el aumento de memoria puede volverse grande
  • Con los compiladores y runtimes más recientes, .NET mostró una gran mejora
  • .NET NativeAOT obtuvo resultados competitivos frente a Rust
  • La native image de GraalVM para Java también mostró buena eficiencia de memoria
  • Las goroutines de Go siguieron mostrando resultados ineficientes en consumo de recursos

Aún no hay comentarios.

Aún no hay comentarios.