Quantidade de memória necessária para executar 1 milhão de tarefas simultâneas em 2024
(hez2010.github.io)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
tokioe outro usandoasync_std. - Ambos são runtimes assíncronos comumente usados em Rust.
- Foram escritos dois programas: um usando
-
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.
- C# oferece suporte a
-
NodeJS
- Usa
Promise.allpara tarefas assíncronas.
- Usa
-
Python
- Usa o módulo
asynciopara executar tarefas assíncronas.
- Usa o módulo
-
Go
- Implementa concorrência com goroutines e usa
WaitGrouppara aguardar as tarefas.
- Implementa concorrência com goroutines e usa
-
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 loopforem vez dejoin_allreduziu o uso de memória pela metade. Rust ficou na liderança absoluta neste benchmark.
1 comentários
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.alle 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-boundExplica 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
ArrayListnão foi especificado, o que cria muitos objetos desnecessáriosExplica 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