paint-brush
NodeJS : 4,8 fois plus rapide si vous revenez aux rappels !by@gemmablack
19,700
19,700

NodeJS : 4,8 fois plus rapide si vous revenez aux rappels !

Gemma Black9m2024/02/12
Read on Terminal Reader

Les rappels sont 4,8 fois plus rapides lorsqu'ils sont exécutés en parallèle via async/wait en parallèle. Et seulement 1,9 fois plus rapide lorsque nous exécutons des rappels séquentiels.
featured image - NodeJS : 4,8 fois plus rapide si vous revenez aux rappels !
Gemma Black HackerNoon profile picture

Ouais, je l'ai dit !


Les rappels sont 4,8 fois plus rapides lorsqu'ils sont exécutés en parallèle sur async/wait en parallèle. Et seulement 1,9 fois plus rapide lorsque nous exécutons des rappels séquentiels.


J'ai quelque peu modifié cet article après avoir reçu des commentaires utiles et aimables sur mon test douteux. 😂🙏


Merci de Ricardo Lopés et Ryan Poé pour avoir pris le temps d’orienter les benchmarks dans la bonne direction. Mon premier faux pas était que je n'attendais pas réellement la fin de l'exécution du code, ce qui faussait follement les résultats. La seconde était que je comparais des exécutions parallèles à des exécutions séquentielles, ce qui rend les benchmarks sans valeur.


C'est donc le deuxième tour qui corrige mes erreurs initiales. Auparavant, je disais :


À l’origine, j’ai écrit que NodeJS est 34,7 fois plus rapide si l’on revient aux rappels ! 🤣 Faux.


Pas aussi impressionnant que mes mauvais benchmarks précédents (et voir les commentaires pour le contexte), mais toujours une différence considérable.

Alors qu'est-ce que j'ai testé exactement ?

J'ai comparé les rappels aux promesses et à l'async/wait lors de la lecture d'un fichier, 10 000 fois. Et c'est peut-être un test idiot, mais je voulais savoir ce qui est plus rapide en E/S.


Ensuite, j'ai finalement comparé les rappels dans Node.js avec Go !


Maintenant, devinez qui a gagné ?


Je ne serai pas méchant. TLDR . Golang!


Plus bas, c'est mieux. Les résultats sont en ms .


Maintenant, de vrais benchmarkers ? Allez-y doucement avec moi sur celui-ci. Mais s'il vous plaît, laissez vos commentaires pour faire de moi une meilleure personne.

Tout le monde n’arrête pas de dire que Node.js est lent !

Et ça me dérange.


Car que signifie ralentir ? Comme pour tous les benchmarks, le mien est contextuel.


J'ai commencé à lire sur le Boucle d'événement , juste pour commencer à comprendre Comment ça fonctionne .


Mais la principale chose que j'ai comprise est que Node.js transmet les tâches d'E/S dans une file d'attente située en dehors du thread exécutable principal de Node.js. Cette file d'attente s'exécute sur C pur . Un certain nombre de threads pourraient potentiellement gérer ces opérations d’E/S. Et c'est là que Node.js peut briller, en gérant les E/S.


Les promesses, cependant, sont gérées dans le thread exécutable principal unique. Et async/wait, c'est bien, promet mais maintenant avec le blocage ajouté.

Boucle d'événements composée de 6 files d'attente différentes. Depuis : https://www.builder.io/blog/visual-guide-to-nodejs-event-loop

Alors les rappels sont-ils plus rapides que les promesses ?

Mettons-le à l'épreuve.


Tout d'abord. Ma machine ! Compléments du travail avec Kamma . Il est important de noter avec quelles ressources nous travaillons. Beaucoup de mémoire et de CPU.

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

Nous avons donc un fichier text.txt avec un message original , Hello, world .

 echo "Hello, world" > text.txt

Et nous lirons ce fichier texte en utilisant Node.js natif, ce qui signifie zéro dépendance de module de nœud car nous ne voulons pas ralentir la vitesse avec les objets les plus lourds de l'univers.

Rappels

Rappels parallèles

Commençons par les rappels parallèles . Je m'intéresse à la rapidité avec laquelle le même fichier peut être lu le plus rapidement possible, d'un seul coup. Et quoi de plus rapide que le parallèle ?

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

Rappels séquentiels

Deuxièmement, nous avons à nouveau des rappels, mais séquentiels (ou plutôt bloquants). Je m'intéresse à la rapidité avec laquelle le même fichier peut être lu séquentiellement. N'ayant pas effectué de rappels depuis des lustres, c'était amusant de réessayer. Mais ça n'a pas l'air joli.

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

Asynchrone/Attente

Ensuite, nous avons async/wait. Ma façon préférée de travailler avec Nodejs.

Asynchrone/attente parallèle

C'est aussi parallèle que possible avec async/await. Je charge toutes les opérations readFile dans un tableau et je les attends toutes en utilisant 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"); }) }) });

Asynchrone/attente séquentielle

C’était le plus simple et le plus concis à écrire.

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

Promesses

Enfin, nous avons des promesses sans async/wait. J'ai depuis longtemps arrêté de les utiliser au profit de async/await mais je voulais savoir s'ils étaient performants ou non.

Des promesses parallèles

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

Des promesses séquentielles.

Encore une fois, nous voulons attendre l'exécution de toutes les opérations 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() } }) } });

Et voilà ! Résultats 🎉! Je l'ai même exécuté plusieurs fois pour obtenir une meilleure lecture.

J'ai exécuté chaque test en faisant :

 node --test <file>.mjs

Lire un fichier 10 000 fois avec des rappels est plus de 5,8 fois plus rapide qu'avec async/wait en parallèle ! C'est aussi 4,7x plus rapide qu'avec des promesses en parallèle !


Ainsi, au pays Node.js, les rappels sont plus performants !

Go est-il désormais plus rapide que Node.js ?

Eh bien, je n'écris pas en Go, donc cela peut être un code vraiment terrible car j'ai demandé à ChatGPT de m'aider et pourtant, cela semble plutôt correct.


Hé ho. Allons-y. Notre code 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) }

Et nous l'exécutons comme ceci :

 go run main.go

Et les résultats ?

 Test execution time: 58.877125ms

🤯 Go est 4,9 fois plus rapide que Node.js en utilisant des rappels séquentiels. Node.js ne se rapproche qu'avec une exécution parallèle.


Node.js Async/await est 9,2 fois plus lent que Go.


Donc oui. Node.js est plus lent. Pourtant, 10 000 fichiers en moins de 300 ms ne sont pas à dédaigner. Mais j'ai été touché par la rapidité de Go !

Maintenant, juste une remarque. Ai-je de mauvais repères ?

J'avais vraiment de terribles benchmarks. Merci encore à Ricardo et Ryan.


Oui je l'ai fait. J'espère que maintenant ils vont mieux.


Mais vous vous demandez peut-être qui va réellement lire le même fichier, encore et encore ? Mais pour un test relatif entre les choses, j'espère que c'est une comparaison utile.


Je ne sais pas non plus combien de threads Node.js utilise.


Je ne sais pas comment mes cœurs de processeur affectent les performances de Go vs Node.js.


Je pourrais simplement louer une machine AWS avec un seul cœur et comparer.


Est-ce parce que je suis sur Mac M1 ?


Comment Node.js fonctionnerait-il sur Linux ou... Windows ? 😱


Et il y a l'aspect pratique de, oui, lire un fichier est une chose, mais à un moment donné, vous devez de toute façon attendre que le fichier soit lu pour faire quelque chose avec les données qu'il contient. Ainsi, la vitesse sur le thread principal est toujours assez importante.

Maintenant, voulez-vous vraiment utiliser les rappels ?

Je veux dire, est-ce que tu le veux vraiment, vraiment ?


Je ne sais pas. Je ne veux absolument dire à personne quoi faire.

Mais j'aime la syntaxe claire de async/awaits.


Ils ont l'air mieux.


Ils lisent mieux.


Je sais que mieux est subjectif ici, mais je me souviens de l'enfer des rappels, et j'étais reconnaissant lorsque les promesses ont vu le jour. Cela a rendu Javascript supportable.


Désormais, Golang est clairement plus rapide que Node.js à son optimal, avec des rappels et avec async/await, de 9,2x ! Donc si nous voulons une bonne lisibilité et performances, Golang est le gagnant. Cependant, j'aimerais savoir à quoi ressemble Golang sous le capot.


N'importe qui. C'était amusant. Il s'agissait plutôt d'un exercice pour m'aider à comprendre comment fonctionnent les rappels et les E/S dans la boucle d'événements.

Alors pour me déconnecter

Node.js est-il lent ? Ou utilisons-nous simplement Node.js en mode lent ?


Probablement là où les performances comptent, Golang vaut le coup. J'envisagerai certainement davantage d'utiliser Golang à l'avenir.


Apparaît également ici .