2 puntos por GN⁺ 2024-11-30 | 1 comentarios | Compartir por WhatsApp

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 tokio y otro usando async_std.
    • Ambos son runtimes asíncronos de uso común en Rust.
  • 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.
  • NodeJS

    • Usa Promise.all para tareas asíncronas.
  • Python

    • Usa el módulo asyncio para realizar tareas asíncronas.
  • Go

    • Implementa concurrencia con goroutines y usa WaitGroup para esperar las tareas.
  • 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 bucle for en lugar de join_all redujo el uso de memoria a la mitad. Rust fue el líder absoluto en este benchmark.

1 comentarios

 
GN⁺ 2024-11-30
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.all y 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 CPU

  • Se 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 innecesarios

  • Se 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