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 etRyan 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.
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.
Et ça me dérange.
Car que signifie ralentir ? Comme pour tous les benchmarks, le mien est contextuel.
J'ai commencé à lire sur le
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
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é.
Mettons-le à l'épreuve.
Tout d'abord. Ma machine ! Compléments du travail avec
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.
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() } }) } });
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) });
Ensuite, nous avons async/wait. Ma façon préférée de travailler avec Nodejs.
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"); }) }) });
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"); } });
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.
// > 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() }) });
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 !
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 !
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.
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.
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 .