2 pontos por GN⁺ 2024-11-30 | 1 comentários | Compartilhar no WhatsApp

Benchmark

  • O que é uma coroutine?

    • Coroutine é um componente de programa de computador que pode pausar e retomar a execução de um programa, generalizando sub-rotinas para multitarefa cooperativa.
    • É adequada para implementar componentes de programa como tarefas cooperativas, exceções, event loops, iteradores, listas infinitas e pipes.
  • Rust

    • Foram escritos dois programas: um usando tokio e outro usando async_std.
    • Ambos são runtimes assíncronos comumente usados em Rust.
  • C#

    • C# oferece suporte a async/await, de forma semelhante ao Rust.
    • Desde o .NET 7, fornece compilação NativeAOT, permitindo executar código gerenciado sem VM.
  • NodeJS

    • Usa Promise.all para tarefas assíncronas.
  • Python

    • Usa o módulo asyncio para executar tarefas assíncronas.
  • Go

    • Implementa concorrência com goroutines e usa WaitGroup para aguardar as tarefas.
  • Java

    • Desde o JDK 21, oferece virtual threads, um conceito semelhante a goroutines.
    • É possível gerar imagens nativas com GraalVM.

Ambiente de teste

  • Hardware: Intel(R) Core(TM) i7-13700K de 13ª geração
  • Sistema operacional: 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 memória

    • Rust, C# (NativeAOT) e Go foram compilados como binários nativos e usam pouca memória.
    • Java (imagem nativa do GraalVM) também mostrou bom desempenho, mas usa mais memória do que outras linguagens compiladas estaticamente.
  • 10 mil tarefas

    • Em Rust, o uso de memória quase não aumenta.
    • C# (NativeAOT) também usa pouca memória.
    • Go consome mais memória do que o esperado.
  • 100 mil tarefas

    • Rust e C# mostram bom desempenho.
    • C# (NativeAOT) usa menos memória do que Rust.
  • 1 milhão de tarefas

    • C# supera todas as linguagens e usa a menor quantidade de memória.
    • Rust também tem excelente eficiência de memória.
    • Go usa mais memória em comparação com outras linguagens.

Conclusão

  • Um grande número de tarefas simultâneas pode consumir uma quantidade considerável de memória, mesmo sem executar operações complexas.
  • As melhorias no .NET e no NativeAOT se destacam, e a imagem nativa de Java construída com GraalVM também mostra excelente eficiência de memória.
  • Goroutines ainda são ineficientes em termos de consumo de recursos.

Apêndice

  • Em Rust (tokio), usar um loop for em vez de join_all reduziu o uso de memória pela metade. Rust ficou na liderança absoluta neste benchmark.

1 comentários

 
GN⁺ 2024-11-30
Comentários do Hacker News
  • O benchmark não reflete adequadamente a diferença entre os modelos de processamento assíncrono de Node e Go. O Node usa Promise.all e o Go usa goroutines, então há diferenças. Seria interessante comparar a diferença no uso de memória entre I/O assíncrona e tarefas CPU-bound

  • Explica a diferença entre uma "tarefa que espera por 10 segundos" e uma "tarefa que acorda após 10 segundos". O uso de memória do código em Go difere bastante em comparação com os outros códigos

  • Sugere, para uma comparação justa entre Go e Node, usar goroutines para agendar timers e outras goroutines para processar os sinais desses timers. Menciona que é estranho Bun e Deno não terem sido incluídos no Node

  • Muitas tarefas concorrentes podem consumir muita memória, mas se os dados por tarefa forem de alguns KB ou mais, o overhead de memória do escalonador se torna desprezível

  • Dependendo da definição de "tarefas concorrentes", o uso de memória pode variar. Em uma implementação eficiente, 1M de tarefas concorrentes exigem cerca de 200MB

  • Aponta que o Go fica mais de 2x atrás do Java em uso de memória e menciona que o benchmark não representa programas reais

  • Comparar linguagens com código simples pode ser injusto com os desenvolvedores, e recomenda adicionar trabalho real para medir diferenças de uso de memória e escalonamento

  • Diz que benchmarks desse tipo frequentemente estão cheios de erros e que não entende a motivação de quem publica esse tipo de benchmark

  • O benchmark de Java pode estar incorreto, já que o tamanho inicial do ArrayList não foi especificado, o que cria muitos objetos desnecessários

  • Explica por que o código assíncrono em Rust termina mais rápido do que o esperado: tokio::time::sleep() rastreia o momento em que o future foi criado