¡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 yryan 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.
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.
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
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
Las promesas, sin embargo, se manejan en el único hilo ejecutable principal. Y async/await, es una promesa, pero ahora con bloqueo agregado.
Pongámoslo a prueba.
Antes que nada. ¡Mi máquina ! Complementos de trabajar con
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.
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() } }) } });
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) });
Luego tenemos async/await. Mi forma favorita de trabajar con Nodejs.
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"); }) }) });
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"); } });
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.
// > 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() }) });
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!
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!
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.
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.
¿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í .