paint-brush
NodeJS:如果您返回回调,速度会快 4.8 倍!经过@gemmablack
20,095 讀數
20,095 讀數

NodeJS:如果您返回回调,速度会快 4.8 倍!

经过 Gemma Black9m2024/02/12
Read on Terminal Reader

太長; 讀書

并行运行回调时,回调速度比并行运行 async/await 快 4.8 倍。当我们运行顺序回调时,速度仅提高 1.9 倍。
featured image - NodeJS:如果您返回回调,速度会快 4.8 倍!
Gemma Black HackerNoon profile picture

是的,我说过了!


并行运行回调时,回调速度比并行运行 async/await 快4.8 倍。当我们运行顺序回调时,速度仅提高了1.9 倍


在我收到一些关于我的不可靠测试的有用和善意的评论后,我对这篇文章进行了一些修改。 😂🙏


谢谢至里卡多·洛佩斯瑞恩·坡感谢您花时间将基准引向正确的方向。我的第一个失误是我实际上并没有等待代码执行完成,这严重扭曲了结果。第二个是我正在比较并行运行时和顺序运行时,这使得基准测试毫无价值。


这是第二轮,它解决了我最初的错误。之前我说过:


最初,我写道,如果我们回到回调,NodeJS 的速度会快 34.7 倍! 🤣 错了。


不像我之前的糟糕基准那么令人印象深刻(并查看上下文的评论),但仍然有相当大的差异。

那么我到底测试了什么?

我在读取文件时将回调与 Promise 和 async/await 进行了 10,000 次比较。也许这是一个愚蠢的测试,但我想知道,哪个 I/O 速度更快。


然后我终于将 Node.js 中的回调与 Go 进行了比较!


现在,猜猜谁赢了?


我不会刻薄的。太长了。戈朗!


越低越好。结果以ms


现在,真正的基准测试者?在这件事上对我宽容一些。但请留下您的评论,让我成为一个更好的人。

大家都在说 Node.js 很慢!

这让我很烦恼。


因为慢意味着什么?与所有基准一样,我的基准是上下文相关的。


我开始阅读有关事件循环,只是为了开始理解怎么运行的


但我主要了解的是,Node.js 将 I/O 任务传递到位于 Node.js 主可执行线程外部的队列上。该队列运行于纯C 。许多线程可能会处理这些 I/O 操作。这就是 Node.js 可以发挥作用的地方:处理 I/O。


然而,承诺是在主单个可执行线程中处理的。异步/等待,很好,承诺但现在添加了阻塞。

事件循环由 6 个不同的队列组成。来自:https://www.builder.io/blog/visual-guide-to-nodejs-event-loop

那么回调比承诺更快吗?

让我们来测试一下。


首先。我的机器!与他人合作的补充业力。请务必注意我们正在使用哪些资源。充足的内存和CPU。

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

所以我们有一个text.txt文件,其中包含原始消息Hello, world

 echo "Hello, world" > text.txt

我们将使用本机 Node.js 读取此文本文件,这意味着零节点模块依赖性,因为我们不想因为宇宙中最重的物体而拖慢速度。

回调

并行回调

首先,让我们从并行回调开始。我感兴趣的是能够以多快的速度一次性读取同一个文件。什么比并行更快?

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

顺序回调

其次,我们再次有回调,但是是顺序的(或者更确切地说是阻塞的)。我感兴趣的是顺序读取同一文件的速度。已经很久没有进行回调调用回调了,再次尝试很有趣。虽然,看起来并不漂亮。

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

异步/等待

然后我们有异步/等待。我最喜欢的使用 Nodejs 的方式。

并行异步/等待

这是我可以通过 async/await 获得的最大并行度。我将所有readFile操作加载到一个数组中,并使用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"); }) }) });

顺序异步/等待

这是写起来最简单、最简洁的一篇。

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

承诺

最后,我们有没有 async/await 的 Promise。我早已停止使用它们,转而使用async/await ,但我对它们是否高性能感兴趣。

平行的承诺

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

连续的承诺。

同样,我们要等待所有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() } }) } });

瞧!结果🎉!我什至运行了几次以获得更好的阅读效果。

我通过执行以下操作来运行每个测试:

 node --test <file>.mjs

使用回调读取文件 10,000 次比并行使用 async/await 快 5.8 倍以上!它也比并行 Promise 快 4.7 倍!


因此,在 Node.js 领域,回调的性能更高

现在 Go 比 Node.js 更快吗?

好吧,我不是用 Go 编写的,所以这可能是真正糟糕的代码,因为我要求 ChatGPT 帮助我,但它看起来相当不错。


嘿嗬。我们走吧。我们的 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) }

我们这样运行它:

 go run main.go

结果如何?

 Test execution time: 58.877125ms

🤯 使用顺序回调,Go 比 Node.js 快 4.9 倍。 Node.js 只有通过并行执行才能接近。


Node.js Async/await 比 Go 慢 9.2 倍。


所以是的。 Node.js 速度较慢。尽管如此,在 300 毫秒内处理 10,000 个文件也不容小觑。但 Go 的速度让我感到谦卑!

现在只是一个旁注。我有不好的基准吗?

我确实有糟糕的基准。再次感谢里卡多和瑞安。


是的,我做到了。希望现在他们更好了。


但您可能会问,谁真的会一遍又一遍地读取同一个文件?但对于事物之间的相对测试,我希望这是一个有用的比较。


我也不知道 Node.js 使用了多少个线程。


我不知道我的 CPU 核心如何影响 Go 与 Node.js 的性能。


我可以租一台只有一个核心的 AWS 机器并进行比较。


是因为我用的是 Mac M1 吗?


Node.js 在 Linux 或 Windows 上的执行情况如何? 😱


还有一个实用性,是的,读取文件是一回事,但在某些时候,您必须等待读取文件才能对文件中的数据执行某些操作。因此,主线程的速度仍然非常重要。

现在,您真的想使用回调吗?

我的意思是,你真的真的想要吗?


我不知道。我绝对不想告诉任何人该怎么做。

但我喜欢 async/await 简洁的语法。


他们看起来更好了。


他们读得更好。


我知道这里的更好是主观的,但我记得回调地狱,当承诺出现时我很感激。它让 Javascript 变得可以忍受。


现在,Golang 在最佳状态下明显比 Node.js 快,在回调和异步/等待方面,快了 9.2 倍!因此,如果我们想要良好的可读性和性能,Golang 是赢家。尽管如此,我很想了解 Golang 的底层是什么样的。


任何人。这很有趣。这更多的是帮助我理解回调和 I/O 在事件循环中如何工作的练习。

所以要退出

Node.js 慢吗?或者我们只是在慢速模式下使用 Node.js?


也许在性能很重要的地方,Golang 值得一跳。将来我肯定会更多地考虑使用 Golang。


也出现在这里