paint-brush
NodeJS: 4,8x schneller, wenn Sie zu Rückrufen zurückkehren!von@gemmablack
20,095 Lesungen
20,095 Lesungen

NodeJS: 4,8x schneller, wenn Sie zu Rückrufen zurückkehren!

von Gemma Black9m2024/02/12
Read on Terminal Reader

Zu lang; Lesen

Rückrufe sind 4,8-mal schneller, wenn sie parallel über async/await parallel ausgeführt werden. Und nur 1,9-mal schneller, wenn wir sequenzielle Rückrufe ausführen.
featured image - NodeJS: 4,8x schneller, wenn Sie zu Rückrufen zurückkehren!
Gemma Black HackerNoon profile picture

Ja, ich habe es gesagt!


Rückrufe sind 4,8-mal schneller, wenn sie parallel über async/await parallel ausgeführt werden. Und nur 1,9-mal schneller, wenn wir sequentielle Rückrufe ausführen.


Ich habe diesen Artikel etwas geändert, nachdem ich einige hilfreiche und freundliche Kommentare zu meinem fragwürdigen Test erhalten habe. 😂🙏


Vielen Dank Ricardo Lopes Und Ryan Poe dafür, dass Sie sich die Zeit genommen haben, die Maßstäbe in die richtige Richtung zu lenken. Mein erster Fauxpas war, dass ich nicht wirklich darauf gewartet habe, dass die Ausführung des Codes abgeschlossen ist, was die Ergebnisse völlig verzerrt hat. Zweitens habe ich parallele mit sequentiellen Laufzeiten verglichen, was die Benchmarks wertlos macht.


Das ist also Runde 2, die meine anfänglichen Fehler behebt. Zuvor sagte ich:


Ursprünglich habe ich geschrieben, dass NodeJS 34,7-mal schneller ist, wenn wir zu Rückrufen zurückkehren! 🤣 Falsch.


Nicht so beeindruckend wie meine schlechten Benchmarks zuvor (siehe Kommentare zum Kontext), aber immer noch ein beträchtlicher Unterschied.

Was genau habe ich also getestet?

Ich habe Rückrufe 10.000 Mal mit Versprechen und Async/Warten beim Lesen einer Datei verglichen. Und vielleicht ist das ein dummer Test, aber ich wollte wissen, was bei I/O schneller ist.


Dann habe ich endlich Callbacks in Node.js mit Go! verglichen.


Ratet mal, wer gewonnen hat?


Ich werde nicht gemein sein. TLDR . Golang!


Weniger ist besser. Die Ergebnisse sind in ms angegeben.


Nun, echte Benchmarker? Seien Sie in dieser Sache vorsichtig mit mir. Aber bitte hinterlassen Sie Ihre Kommentare, um mich zu einem besseren Menschen zu machen.

Jeder sagt immer, dass Node.js langsam ist!

Und es nervt mich.


Denn was heißt langsam? Wie bei allen Benchmarks ist meiner kontextbezogen.


Ich fing an, darüber zu lesen Ereignisschleife , nur um überhaupt zu verstehen wie es funktioniert .


Aber das Wichtigste, was ich verstanden habe, ist, dass Node.js E/A-Aufgaben an eine Warteschlange weiterleitet, die sich außerhalb des ausführbaren Hauptthreads von Node.js befindet. Diese Warteschlange läuft weiter reines C . Möglicherweise könnten mehrere Threads diese E/A-Vorgänge verarbeiten. Und hier kann Node.js glänzen, wenn es um die I/O-Verwaltung geht.


Versprechen werden jedoch im einzigen ausführbaren Hauptthread verarbeitet. Und async/await ist gut, versprochen, aber jetzt mit hinzugefügter Blockierung.

Ereignisschleife bestehend aus 6 verschiedenen Warteschlangen. Von: https://www.builder.io/blog/visual-guide-to-nodejs-event-loop

Sind Rückrufe also schneller als Versprechen?

Stellen wir es auf die Probe.


Erst einmal. Meine Maschine ! Ergänzungen zur Arbeit mit Kamma . Es ist wichtig zu beachten, mit welchen Ressourcen wir arbeiten. Viel Speicher und CPU.

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

Wir haben also eine text.txt Datei mit einer Originalnachricht : Hello, world .

 echo "Hello, world" > text.txt

Und wir werden diese Textdatei mit nativem Node.js lesen, was bedeutet, dass es keine Knotenmodulabhängigkeiten gibt, weil wir die Geschwindigkeit bei den schwersten Objekten im Universum nicht verringern wollen.

Rückrufe

Parallele Rückrufe

Beginnen wir zunächst mit parallelen Rückrufen. Mich interessiert, wie schnell die gleiche Datei möglichst schnell und auf einmal gelesen werden kann. Und was ist schneller als parallel?

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

Sequentielle Rückrufe

Zweitens haben wir wieder Rückrufe, aber sequentiell (oder besser gesagt blockierend). Mich interessiert, wie schnell dieselbe Datei nacheinander gelesen werden kann. Nachdem ich schon seit Ewigkeiten keine Rückrufe mehr durchgeführt habe, hat es Spaß gemacht, es noch einmal zu versuchen. Allerdings sieht es nicht schön aus.

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

Asynchron/Warten

Dann haben wir async/await. Meine liebste Art, mit Nodejs zu arbeiten.

Parallel asynchron/warten

Es ist so parallel, wie ich es mit async/await erreichen kann. Ich lade alle readFile Vorgänge in ein Array und erwarte sie alle mit 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"); }) }) });

Sequentielles Async/Warten

Dies war am einfachsten und prägnantesten zu schreiben.

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

Versprechen

Endlich haben wir Versprechen ohne Async/Await. Ich habe sie schon lange nicht mehr zugunsten von async/await verwendet, aber mich interessierte, ob sie performant waren oder nicht.

Parallele Versprechen

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

Aufeinanderfolgende Versprechen.

Auch hier möchten wir auf die Ausführung aller readFile Vorgänge warten.

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

Und voilà! Ergebnisse 🎉! Ich habe es sogar ein paar Mal laufen lassen, um es besser lesen zu können.

Ich habe jeden Test folgendermaßen durchgeführt:

 node --test <file>.mjs

Das 10.000-malige Lesen einer Datei mit Rückrufen ist über 5,8-mal schneller als mit parallelem Async/Await! Es ist außerdem 4,7-mal schneller als mit parallelen Versprechen!


Im Node.j-Bereich sind Rückrufe also leistungsfähiger!

Ist Go jetzt schneller als Node.js?

Nun, ich schreibe nicht in Go, also ist das vielleicht ein wirklich schrecklicher Code, weil ich ChatGPT um Hilfe gebeten habe, und dennoch scheint er ziemlich anständig zu sein.


Hey ho. Lass uns gehen. Unser Golang-Code.

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

Und wir führen es so aus:

 go run main.go

Und die Ergebnisse?

 Test execution time: 58.877125ms

🤯 Go ist mit sequentiellen Rückrufen 4,9-mal schneller als Node.js. Node.js kommt nur bei paralleler Ausführung nahe.


Node.js Async/await ist 9,2x langsamer als Go.


Also ja. Node.js ist langsamer. Dennoch sind 10.000 Dateien in weniger als 300 ms nicht zu verachten. Aber Gos Schnelligkeit hat mich beeindruckt!

Jetzt nur noch eine Randbemerkung. Habe ich schlechte Benchmarks?

Ich hatte wirklich schreckliche Benchmarks. Nochmals vielen Dank an Ricardo und Ryan.


Ja, habe ich. Hoffentlich sind sie jetzt besser.


Aber Sie fragen sich vielleicht: Wer liest wirklich immer und immer wieder dieselbe Datei? Aber für einen relativen Test zwischen den Dingen hoffe ich, dass es ein hilfreicher Vergleich ist.


Ich weiß auch nicht, wie viele Threads Node.js verwendet.


Ich weiß nicht, wie sich meine CPU-Kerne auf die Leistung von Go im Vergleich zu Node.js auswirken.


Ich könnte einfach eine AWS-Maschine mit einem Kern mieten und vergleichen.


Liegt es daran, dass ich einen Mac M1 verwende?


Wie würde Node.js unter Linux oder ... Windows funktionieren? 😱


Und da ist noch die praktische Seite: Ja, das Lesen einer Datei ist eine Sache, aber irgendwann muss man sowieso warten, bis die Datei gelesen ist, um etwas mit den Daten in der Datei zu tun. Daher ist die Geschwindigkeit im Hauptthread immer noch ziemlich wichtig.

Wollen Sie nun wirklich Rückrufe verwenden?

Ich meine, willst du das wirklich, wirklich?


Ich weiß nicht. Ich möchte auf keinen Fall irgendjemandem sagen, was er tun soll.

Aber ich mag die saubere Syntax von async/awaits.


Sie sehen besser aus.


Sie lesen besser.


Ich weiß, besser ist hier subjektiv, aber ich erinnere mich an die Hölle der Rückrufe, und ich war dankbar, als Versprechen zustande kamen. Es machte Javascript erträglich.


Jetzt ist Golang im Optimum deutlich schneller als Node.js, mit Rückrufen und mit Async/Await, und zwar um das 9,2-fache! Wenn wir also eine gute Lesbarkeit und Leistung wollen, ist Golang der Gewinner. Allerdings würde ich gerne erfahren, wie Golang unter der Haube aussieht.


Irgendjemand. Das hat Spaß gemacht. Es war eher eine Übung, die mir helfen sollte zu verstehen, wie Rückrufe und E/A in der Ereignisschleife funktionieren.

Also abmelden

Ist Node.js langsam? Oder verwenden wir Node.js nur im langsamen Modus?


Wo es auf die Leistung ankommt, ist Golang wahrscheinlich den Sprung wert. Ich werde in Zukunft sicherlich mehr über die Verwendung von Golang nachdenken.


Erscheint auch hier .