Sim, eu disse isso!
Os retornos de chamada são 4,8x mais rápidos quando executados em paralelo em async/await em paralelo. E apenas 1,9x mais rápido quando executamos retornos de chamada sequenciais.
Modifiquei um pouco este artigo depois de receber alguns comentários úteis e gentis sobre meu teste duvidoso. 😂🙏
Obrigada por
Ricardo Lopes eRyan Poe por reservar um tempo para orientar os benchmarks na direção certa. Minha primeira gafe foi que eu não estava realmente esperando a execução do código terminar, o que distorceu loucamente os resultados. A segunda foi que eu estava comparando tempos de execução paralelos e sequenciais, o que torna os benchmarks inúteis.
Portanto, esta é a segunda rodada, que aborda meus erros iniciais. Anteriormente eu disse:
Originalmente, escrevi, que o NodeJS é 34,7x mais rápido se voltarmos aos retornos de chamada! 🤣 Errado.
Não tão impressionante quanto meus benchmarks ruins anteriores (e veja os comentários para contextualizar), mas ainda assim uma diferença considerável.
Comparei retornos de chamada com promessas e async/await ao ler um arquivo, 10.000 vezes. E talvez seja um teste bobo, mas eu queria saber qual é o mais rápido na E/S.
Então finalmente comparei os retornos de chamada no Node.js to Go!
Agora, adivinhe quem ganhou?
Eu não serei mau. TLDR . Golang!
Menor é melhor. Os resultados estão em
ms
.
Agora, verdadeiros benchmarkers? Vá com calma comigo neste caso. Mas por favor, deixe seus comentários para me tornar uma pessoa melhor.
E isso me incomoda.
Porque o que significa lento? Tal como acontece com todos os benchmarks, o meu é contextual.
Comecei a ler sobre
Mas a principal coisa que entendi é que o Node.js passa tarefas de E/S para uma fila que fica fora do thread executável principal do Node.js. Esta fila é executada em
As promessas, no entanto, são tratadas no único thread executável principal. E async/await, está bem, promete, mas agora com bloqueio adicionado.
Vamos colocar isso à prova.
Primeiramente. Minha máquina ! Complementos de trabalhar com
MacBook Pro (14-inch, 2021) Chip Apple M1 Pro Memory 32 GB Cores 10 NodeJS v20.8.1
Portanto, temos um arquivo text.txt
com uma mensagem original , Hello, world
.
echo "Hello, world" > text.txt
E leremos este arquivo de texto usando Node.js nativo, o que significa zero dependências de módulo de nó porque não queremos diminuir a velocidade com os objetos mais pesados do universo.
Primeiro, vamos começar com retornos de chamada paralelos . Estou interessado em saber a rapidez com que o mesmo arquivo pode ser lido o mais rápido possível, tudo de uma vez. E o que é mais rápido que o paralelo?
// > file-callback-parallel.test.mjs import test from 'node:test'; import assert from 'node:assert'; import fs from "node:fs"; test('reading file 10,000 times with callback parallel', (t, done) => { let count = 0; for (let i = 0; i < 10000; i++) { fs.readFile("./text.txt", { encoding: 'utf-8'}, (err, data) => { assert.strictEqual(data, "Hello, world"); count++ if (count === 10000) { done() } }) } });
Em segundo lugar, temos retornos de chamada novamente, mas sequenciais (ou melhor, bloqueadores). Estou interessado em saber a rapidez com que o mesmo arquivo pode ser lido sequencialmente. Não tendo feito retornos de chamada há muito tempo, foi divertido tentar novamente. Embora não pareça bonito.
// > file-callback-blocking.test.mjs import test from 'node:test'; import assert from 'node:assert'; import fs from "node:fs"; let read = (i, callback) => { fs.readFile("./text.txt", { encoding: 'utf-8'}, (err, data) => { assert.strictEqual(data, "Hello, world"); i += 1 if (i === 10000) { return callback() } read(i, callback) }) } test('reading file 10,000 times with callback blocking', (t, done) => { read(0, done) });
Então temos async/await. Minha maneira favorita de trabalhar com Nodejs.
É o mais paralelo possível com async/await. Carrego todas as operações readFile
em um array e aguardo todas elas usando Promise.all
.
// > file-async-parallel.test.mjs import test from 'node:test'; import assert from 'node:assert'; import fs from "node:fs/promises"; test('reading file 10,000 times with async parallel', async (t) => { let allFiles = [] for (let i = 0; i < 10000; i++) { allFiles.push(fs.readFile("./text.txt", { encoding: 'utf-8'})) } return await Promise.all(allFiles) .then(allFiles => { return allFiles.forEach((data) => { assert.strictEqual(data, "Hello, world"); }) }) });
Este foi o mais fácil e conciso de escrever.
// > file-async-blocking.test.mjs import test from 'node:test'; import assert from 'node:assert'; import fs from "node:fs/promises"; test('reading file 10,000 times with async blocking', async (t) => { for (let i = 0; i < 10000; i++) { let data = await fs.readFile("./text.txt", { encoding: 'utf-8'}) assert.strictEqual(data, "Hello, world"); } });
Finalmente, temos promessas sem async/await. Há muito tempo parei de usá-los em favor de async/await
, mas estava interessado em saber se eles tinham desempenho ou não.
// > file-promise-parallel.test.mjs import test from 'node:test'; import assert from 'node:assert'; import fs from "node:fs/promises"; test('reading file 10,000 times with promise parallel', (t, done) => { let allFiles = [] for (let i = 0; i < 10000; i++) { allFiles.push(fs.readFile("./text.txt", { encoding: 'utf-8'})) } Promise.all(allFiles) .then(allFiles => { for (let i = 0; i < 10000; i++) { assert.strictEqual(allFiles[i], "Hello, world"); } done() }) });
Novamente, queremos aguardar a execução de todas as operações readFile
.
// > file-promise-blocking.test.mjs import test from 'node:test'; import assert from 'node:assert'; import fs from "node:fs/promises"; test('reading file 10,000 times with promises blocking', (t, done) => { let count = 0; for (let i = 0; i < 10000; i++) { let data = fs.readFile("./text.txt", { encoding: 'utf-8'}) .then(data => { assert.strictEqual(data, "Hello, world") count++ if (count === 10000) { done() } }) } });
E pronto! Resultados 🎉! Até corri algumas vezes para obter uma leitura melhor.
Eu executei cada teste fazendo:
node --test <file>.mjs
Ler um arquivo 10.000 vezes com retornos de chamada é 5,8x mais rápido do que com async/await em paralelo! Também é 4,7x mais rápido do que com promessas em paralelo!
Portanto, no terreno do Node.js, os retornos de chamada têm melhor desempenho!
Bem, eu não escrevo em Go, então este pode ser um código realmente terrível porque pedi ao ChatGPT para me ajudar e, ainda assim, parece bastante decente.
Ei, ei. Vamos. Nosso código Golang.
package main import ( "fmt" "io/ioutil" "time" ) func main() { startTime := time.Now() for i := 0; i < 10000; i++ { data, err := ioutil.ReadFile("./text.txt") if err != nil { fmt.Printf("Error reading file: %v\n", err) return } if string(data) != "Hello, world" { fmt.Println("File content mismatch: got", string(data), ", want Hello, world") return } } duration := time.Since(startTime) fmt.Printf("Test execution time: %v\n", duration) }
E nós executamos assim:
go run main.go
E os resultados?
Test execution time: 58.877125ms
🤯 Go é 4,9x mais rápido que Node.js usando retornos de chamada sequenciais. O Node.js só chega perto da execução paralela.
Node.js Async/await é 9,2x mais lento que Go.
Então sim. Node.js é mais lento. Ainda assim, 10.000 arquivos em menos de 300 ms não são motivo de zombaria. Mas fiquei emocionado com a rapidez do Go!
Eu realmente tive benchmarks terríveis. Obrigado novamente a Ricardo e Ryan.
Sim eu fiz. Espero que agora eles estejam melhores.
Mas você pode perguntar: quem realmente vai ler o mesmo arquivo repetidamente? Mas para um teste relativo entre as coisas, espero que seja uma comparação útil.
Também não sei quantos threads o Node.js está usando.
Não sei como os núcleos da minha CPU afetam o desempenho do Go vs Node.js.
Eu poderia simplesmente alugar uma máquina AWS com um núcleo e comparar.
É porque estou no Mac M1?
Qual seria o desempenho do Node.js em um Linux ou... Windows? 😱
E tem a praticidade de, sim, ler um arquivo é uma coisa, mas em algum momento você tem que esperar de qualquer maneira que o arquivo seja lido para fazer algo com os dados contidos no arquivo. Portanto, a velocidade no thread principal ainda é muito importante.
Quero dizer, você realmente quer?
Não sei. Definitivamente não quero dizer a ninguém o que fazer.
Mas gosto da sintaxe limpa de async/awaits.
Eles parecem melhores.
Eles leem melhor.
Eu sei que é subjetivo aqui, mas lembro-me do retorno de chamada e fiquei grato quando as promessas surgiram. Tornou o Javascript suportável.
Agora, Golang é claramente mais rápido que o Node.js no seu nível ideal, com retornos de chamada e com assíncrono/espera, em 9,2x! Portanto, se quisermos boa legibilidade e desempenho, Golang é o vencedor. Embora eu adoraria saber como Golang se parece por baixo do capô.
Qualquer um. Isso foi divertido. Foi mais um exercício para me ajudar a entender como os retornos de chamada e E/S funcionam no Event Loop.
O Node.js é lento? Ou estamos apenas usando o Node.js no modo lento?
Provavelmente onde o desempenho é importante, vale a pena saltar para Golang. Certamente procurarei mais usar Golang no futuro.
Também aparece aqui .