paint-brush
NodeJS: 4,8x mais rápido se você voltar aos retornos de chamada!by@gemmablack
19,696
19,696

NodeJS: 4,8x mais rápido se você voltar aos retornos de chamada!

Gemma Black9m2024/02/12
Read on Terminal Reader

Os retornos de chamada são 4,8x mais rápidos quando executados em paralelo por meio de async/await em paralelo. E apenas 1,9x mais rápido quando executamos retornos de chamada sequenciais.
featured image - NodeJS: 4,8x mais rápido se você voltar aos retornos de chamada!
Gemma Black HackerNoon profile picture

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 e Ryan 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.

Então, o que exatamente eu testei?

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.

Todo mundo fica dizendo que o Node.js é lento!

E isso me incomoda.


Porque o que significa lento? Tal como acontece com todos os benchmarks, o meu é contextual.


Comecei a ler sobre Ciclo de eventos , só para começar a entender como funciona .


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 C puro . Vários threads poderiam potencialmente lidar com essas operações de E/S. E é aí que o Node.js pode brilhar, lidando com E/S.


As promessas, no entanto, são tratadas no único thread executável principal. E async/await, está bem, promete, mas agora com bloqueio adicionado.

Loop de eventos composto por 6 filas diferentes. De: https://www.builder.io/blog/visual-guide-to-nodejs-event-loop

Então, os retornos de chamada são mais rápidos do que as promessas?

Vamos colocar isso à prova.


Primeiramente. Minha máquina ! Complementos de trabalhar com Carma . É importante observar com quais recursos estamos trabalhando. Muita memória e CPU.

 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.

Retornos de chamada

Retornos de chamada paralelos

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() } }) } });

Retornos de chamada sequenciais

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) });

Assíncrono/Aguardar

Então temos async/await. Minha maneira favorita de trabalhar com Nodejs.

Paralelo assíncrono/aguardado

É 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"); }) }) });

Sequencial Assíncrono/Aguarda

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"); } });

Promessas

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.

Promessas paralelas

 // > 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() }) });

Promessas sequenciais.

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!

Agora o Go é mais rápido que o Node.js?

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!

Agora apenas uma observação lateral. Tenho benchmarks ruins?

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.

Agora, você realmente deseja usar retornos de chamada?

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.

Então, para sair

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 .