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 UndRyan 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.
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.
Und es nervt mich.
Denn was heißt langsam? Wie bei allen Benchmarks ist meiner kontextbezogen.
Ich fing an, darüber zu lesen
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
Versprechen werden jedoch im einzigen ausführbaren Hauptthread verarbeitet. Und async/await ist gut, versprochen, aber jetzt mit hinzugefügter Blockierung.
Stellen wir es auf die Probe.
Erst einmal. Meine Maschine ! Ergänzungen zur Arbeit mit
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.
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() } }) } });
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) });
Dann haben wir async/await. Meine liebste Art, mit Nodejs zu arbeiten.
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"); }) }) });
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"); } });
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.
// > 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() }) });
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!
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!
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.
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.
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 .