paint-brush
NodeJS: ¡4,8 veces más rápido si vuelves a las devoluciones de llamada!por@gemmablack
19,996 lecturas
19,996 lecturas

NodeJS: ¡4,8 veces más rápido si vuelves a las devoluciones de llamada!

por Gemma Black9m2024/02/12
Read on Terminal Reader

Demasiado Largo; Para Leer

Las devoluciones de llamada son 4,8 veces más rápidas cuando se ejecutan en paralelo en lugar de async/await en paralelo. Y solo 1,9 veces más rápido cuando ejecutamos devoluciones de llamada secuenciales.
featured image - NodeJS: ¡4,8 veces más rápido si vuelves a las devoluciones de llamada!
Gemma Black HackerNoon profile picture

¡Sí, lo dije!


Las devoluciones de llamada son 4,8 veces más rápidas cuando se ejecutan en paralelo sobre async/await en paralelo. Y solo 1,9 veces más rápido cuando ejecutamos devoluciones de llamada secuenciales.


Modifiqué un poco este artículo después de recibir algunos comentarios útiles y amables sobre mi prueba poco fiable. 😂🙏


gracias a ricardo lopes y ryan poe por tomarse el tiempo para orientar los puntos de referencia en la dirección correcta. Mi primer paso en falso fue que en realidad no estaba esperando a que finalizara la ejecución del código, lo que sesgó enormemente los resultados. La segunda fue que estaba comparando tiempos de ejecución paralelos con secuenciales, lo que hace que los puntos de referencia no tengan valor.


Esta es la segunda ronda que aborda mis errores iniciales. Anteriormente dije:


Originalmente, escribí que NodeJS es 34,7 veces más rápido si volvemos a las devoluciones de llamada. 🤣 Mal.


No es tan impresionante como mis malos puntos de referencia anteriores (y vea los comentarios para conocer el contexto), pero sigue siendo una diferencia considerable.

Entonces, ¿qué probé exactamente?

Comparé las devoluciones de llamada con las promesas y async/await al leer un archivo, 10.000 veces. Y tal vez sea una prueba tonta, pero quería saber cuál es más rápido en E/S.


Luego, finalmente comparé las devoluciones de llamada en Node.js con Go!


Ahora, ¿adivinen quién ganó?


No seré malo. TLDR . ¡Golang!


Más bajo es mejor. Los resultados están en ms .


Ahora bien, ¿verdaderos referentes? Ten cuidado conmigo en esto. Pero por favor deja tus comentarios para hacerme una mejor persona.

¡Todo el mundo sigue diciendo que Node.js es lento!

Y me molesta.


Porque ¿qué significa lento? Como ocurre con todos los puntos de referencia, el mío es contextual.


Empecé a leer sobre el Bucle de eventos , sólo para empezar a entender cómo funciona .


Pero lo principal que he entendido es que Node.js pasa las tareas de E/S a una cola que se encuentra fuera del hilo ejecutable principal de Node.js. Esta cola continúa C pura . Varios subprocesos podrían potencialmente manejar estas operaciones de E/S. Y ahí es donde Node.js puede brillar, manejando E/S.


Las promesas, sin embargo, se manejan en el único hilo ejecutable principal. Y async/await, es una promesa, pero ahora con bloqueo agregado.

Bucle de eventos que consta de 6 colas diferentes. De: https://www.builder.io/blog/visual-guide-to-nodejs-event-loop

Entonces, ¿las devoluciones de llamada son más rápidas que las promesas?

Pongámoslo a prueba.


Antes que nada. ¡Mi máquina ! Complementos de trabajar con kamma . Es importante tener en cuenta con qué recursos estamos trabajando. Mucha memoria y CPU.

 MacBook Pro (14-inch, 2021) Chip Apple M1 Pro Memory 32 GB Cores 10 NodeJS v20.8.1

Entonces tenemos un archivo text.txt con un mensaje original , Hello, world .

 echo "Hello, world" > text.txt

Y leeremos este archivo de texto usando Node.js nativo, lo que significa cero dependencias de módulos de nodo porque no queremos reducir la velocidad con los objetos más pesados del universo.

Devoluciones de llamada

Devoluciones de llamada paralelas

Primero, comencemos con devoluciones de llamada paralelas . Me interesa saber qué tan rápido se puede leer el mismo archivo lo más rápido posible, todo a la vez. ¿Y qué es más rápido que el 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() } }) } });

Devoluciones de llamada secuenciales

En segundo lugar, volvemos a tener devoluciones de llamada, pero secuenciales (o más bien de bloqueo). Me interesa saber qué tan rápido se puede leer secuencialmente el mismo archivo. Como no había hecho devoluciones de llamada durante mucho tiempo, fue divertido intentarlo de nuevo. Aunque no se ve 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) });

Asíncrono/Espera

Luego tenemos async/await. Mi forma favorita de trabajar con Nodejs.

Asíncrono/espera paralelo

Es lo más paralelo que puedo conseguir con async/await. Cargo todas las operaciones readFile en una matriz y las espero 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"); }) }) });

Asíncrono secuencial/espera

Este fue el más fácil y conciso de escribir.

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

Promesas

Finalmente, tenemos promesas sin async/await. Hace mucho que dejé de usarlos en favor de async/await , pero me interesaba saber si funcionaban o no.

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

Promesas secuenciales.

Nuevamente, queremos esperar la ejecución de todas las operaciones 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() } }) } });

¡Y voilá! ¡Resultados 🎉! Incluso lo ejecuté varias veces para obtener una mejor lectura.

Ejecuté cada prueba haciendo:

 node --test <file>.mjs

Leer un archivo 10.000 veces con devoluciones de llamada es 5,8 veces más rápido que con async/await en paralelo. ¡También es 4,7 veces más rápido que con promesas en paralelo!


Entonces, en la tierra de Node.js, ¡las devoluciones de llamada tienen más rendimiento!

¿Ahora Go es más rápido que Node.js?

Bueno, no escribo en Go, así que este puede ser un código realmente terrible porque le pedí ayuda a ChatGPT y, aun así, parece bastante decente.


Hola hola. Vamos. Nuestro 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) }

Y lo ejecutamos así:

 go run main.go

¿Y los resultados?

 Test execution time: 58.877125ms

🤯 Go es 4,9 veces más rápido que Node.js utilizando devoluciones de llamada secuenciales. Node.js sólo se acerca a la ejecución paralela.


Node.js Async/await es 9,2 veces más lento que Go.


Entonces sí. Node.js es más lento. Aun así, no hay que burlarse de 10.000 archivos en menos de 300 ms. ¡Pero me siento honrado por la velocidad de Go!

Ahora sólo una nota al margen. ¿Tengo malos puntos de referencia?

Realmente tuve puntos de referencia terribles. Gracias nuevamente a Ricardo y Ryan.


Sí, lo hice. Ojalá ahora estén mejor.


Pero quizás te preguntes: ¿quién va a leer realmente el mismo archivo una y otra vez? Pero para una prueba relativa entre cosas, espero que sea una comparación útil.


Tampoco sé cuántos subprocesos está usando Node.js.


No sé cómo los núcleos de mi CPU afectan el rendimiento de Go frente a Node.js.


Podría alquilar una máquina AWS con un núcleo y compararla.


¿Es porque estoy en Mac M1?


¿Cómo funcionaría Node.js en Linux o...Windows? 😱


Y está la practicidad de, sí, leer un archivo es una cosa, pero en algún momento, tienes que esperar de todos modos a que se lea el archivo para hacer algo con los datos del archivo. Entonces, la velocidad en el hilo principal sigue siendo bastante importante.

Ahora bien, ¿realmente quieres utilizar devoluciones de llamada?

Quiero decir, ¿de verdad quieres hacerlo?


No sé. Definitivamente no quiero decirle a nadie qué hacer.

Pero me gusta la sintaxis limpia de async/awaits.


Se ven mejor.


Leen mejor.


Sé que mejor es subjetivo aquí, pero recuerdo la devolución de llamada, el infierno, y agradecí cuando las promesas surgieron. Hizo que Javascript fuera soportable.


Ahora, Golang es claramente más rápido que Node.js en su punto óptimo, con devoluciones de llamada y con async/await, ¡9,2 veces más! Entonces, si queremos buena legibilidad y rendimiento, Golang es el ganador. Aunque me encantaría saber cómo se ve Golang debajo del capó.


De todos modos. Esto fue divertido. Fue más bien un ejercicio para ayudarme a comprender cómo funcionan las devoluciones de llamada y las E/S en el bucle de eventos.

Así que para cerrar sesión

¿Node.js es lento? ¿O simplemente estamos usando Node.js en modo lento?


Probablemente donde el rendimiento importa, vale la pena dar el salto a Golang. Sin duda, analizaré más el uso de Golang en el futuro.


También aparece aquí .