- Comparação do uso de memória entre assíncrono e multithread nas linguagens Rust, Go, Java, C#, Python, Node.js e Elixir
- Foi escrito um programa em cada linguagem (com ajuda do ChatGPT) para executar N tarefas que esperam por 10 segundos
- Comparação feita em um Xeon E3 + Ubuntu 22.04
Resultados
- Pegada mínima (experimento com apenas 1 tarefa): Go e Rust exigem menos de 3 MB, Python 17 MB, Java/Node.js cerca de 40 MB, C# 131 MB
- 10 mil tarefas: Rust Tokio 4,6 MB, Rust async-std 8 MB, Go 28,6 MB, Python 40 MB, Rust Threads 48 MB, Node.js 48 MB, Java Virtual Thread 78 MB, Elixir 99 MB, C# 131 MB, Java Threads 244 MB
- 100 mil tarefas (excluindo threads): Rust tokio 23 MB, Rust Async-std 54 MB, Node.js 112 MB, C# 130 MB, Java virtual threads 223 MB, Python 240 MB, Go 269 MB, Elixir 445 MB
- 1 milhão de tarefas: Rust Tokio 213 MB, C# 461 MB, Node.js 494 MB, Rust async-std 527 MB, Java virtual thread 1154 MB, Python 2232 MB, Go 2658 MB, Elixir 4009 MB
Conclusão
- Rust Tokio está em outro nível
- O C# tem uma pegada grande, mas é muito competitivo (chega até a superar Rust)
- Em 1 milhão de tarefas, Go se distancia das virtual threads de Java (invertendo a ideia comum de que Go é mais leve que a JVM)
- Como a análise observou apenas o uso de memória, outros fatores não foram considerados
- Em 1 milhão de tarefas, o overhead para iniciar o trabalho aumenta, e a maior parte dos códigos leva mais de 12 segundos para terminar
- Também serão executados outros benchmarks
9 comentários
Como uso Go e continuo de olho em Rust, este parece ser um benchmark bem significativo para quem se pergunta se realmente vale a pena se adaptar a essa sintaxe tão rígida. Se o Rust consegue aguentar bem mesmo em situações em que o Go morreria por OOM... então certamente vale o investimento. Claro, o problema continua sendo que ainda é bem mais difícil encontrar desenvolvedores Rust...
É verdade que, no Go, cada goroutine recebe uma stack própria (2 KB), então o uso cresce em O(n), o que faz com que ele fique em desvantagem à medida que o número de threads aumenta....
Uma curiosidade menor é com que frequência acontece uma situação com mais de 10 mil threads. Parece que a troca de contexto vai acontecer com mais frequência do que o próprio código rodando....
Fico curioso para saber como isso seria com corrotinas do Kotlin.
O resultado do Elixir é o mais surpreendente; eu sabia que o Erlang consumia apenas algumas centenas de words de memória, sendo mais leve até do que o Go...
Dando uma olhada na documentação oficial do Erlang, vi que são necessárias 338 words para criar um processo Erlang. E, como em um sistema de 64 bits 1 word equivale a 8 bytes, um processo Erlang ocuparia cerca de 2,7 KB (338 × 8 = 2.704) de memória. Como o tamanho de uma stack de goroutine em Go é de cerca de 2,0 KB, parece que o lado do Erlang consome mais memória.
Nesse caso, por um cálculo simples, 1 milhão de processos Erlang deveria ocupar 2,7 GB de memória, mas no benchmark em Elixir apresentado acima foi observado um uso máximo de memória de cerca de 4,0 GB, ou seja, foram usados mais 1,3 GB de memória. Fazendo uma conta simples, isso significa que neste cenário foram usados mais 1,3 KB de memória por processo Erlang, e, embora eu não tenha certeza, fico pensando se quando o número de processos Erlang passa de um certo limite não acaba sendo necessário algum uso adicional de espaço de memória pelo runtime.
Suspeito que talvez seja por causa da reserva de capacidade do mapa em árvore de supervisão ou da fila de mensagens.
Acho que Rust é realmente uma linguagem incrível, desde o paradigma até o desempenho.
A comparação entre abordagens assíncronas e com threads, além de benchmarks que ainda envolvem runtimes de linguagens, pode variar conforme a perspectiva, então veja como referência.
Também vale a pena ler os comentários no HN. https://news.ycombinator.com/item?id=36024209