Nodejs中callback和promise性能差距疑问

发布于 1周前 作者 songsunli 来自 nodejs/Nestjs

Nodejs中callback和promise性能差距疑问

callback

import * as fs from 'fs';
import * as path from 'path';
function list_dir(dir: string) {
    fs.readdir(dir, 'utf-8', (err, files) => {
        files.forEach(file => {
            file = path.join(dir, file)
            fs.stat(file, (err, stat) => {
                if (stat.isDirectory()) {
                    list_dir(file)
                }
                if (stat.isFile()) {
                    file
                    // console.log("%O", file);
                }
            })
        });
    })
}

list_dir('.')

promise

import * as util from 'util';
import * as fs from 'fs';
import * as path from 'path';
async function list_dir(dir: string) {
    const readdir = util.promisify(fs.readdir);
    const stat = util.promisify(fs.stat);
    const files = await readdir(dir, 'utf-8')
    files.forEach(async file => {
        file = path.join(dir, file)
        const state = await stat(file)
        if (state.isDirectory()) {
            await list_dir(file)
        }
        if (state.isFile()) {
            file
            // console.log("%O", file);
        }
    })

}

list_dir(’.’)

性能测试结果

node ➜ /workspaces/typescript $ time node promise.js && time node callback.js

real 0m1.140s user 0m1.136s sys 0m1.292s

real 0m0.377s user 0m0.368s sys 0m0.534s node ➜ /workspaces/typescript $ time node promise.js && time node callback.js

real 0m1.062s user 0m1.132s sys 0m1.184s

real 0m0.538s user 0m0.470s sys 0m0.784s node ➜ /workspaces/typescript $ time node promise.js && time node callback.js

real 0m1.194s user 0m1.221s sys 0m1.308s

real 0m0.436s user 0m0.393s sys 0m0.651s node ➜ /workspaces/typescript $ time node promise.js && time node callback.js

real 0m1.024s user 0m1.165s sys 0m1.027s

real 0m0.416s user 0m0.313s sys 0m0.653s node ➜ /workspaces/typescript $ nodejs --version v16.3.0 node ➜ /workspaces/typescript $ tsc --version Version 4.3.5

有性能差异可以理解,但是这个性能差异过大,请问各位是我的写法有问题,还是其它原因导致的呢?


25 回复

❯ node --version
v14.17.3

❯ time node /tmp/promise.js && time node /tmp/callback.js
node /tmp/promise.js 0.66s user 0.84s system 169% cpu 0.887 total
node /tmp/callback.js 0.42s user 1.13s system 192% cpu 0.806 total
❯ time node /tmp/promise.js && time node /tmp/callback.js
node /tmp/promise.js 0.61s user 0.92s system 169% cpu 0.899 total
node /tmp/callback.js 0.41s user 0.84s system 156% cpu 0.804 total
❯ time node /tmp/promise.js && time node /tmp/callback.js
node /tmp/promise.js 0.46s user 1.03s system 168% cpu 0.886 total
node /tmp/callback.js 0.30s user 0.76s system 134% cpu 0.783 total

❯ time node /tmp/callback.js&& time node /tmp/promise.js
node /tmp/callback.js 0.44s user 0.81s system 160% cpu 0.772 total
node /tmp/promise.js 0.64s user 0.89s system 170% cpu 0.898 total
❯ time node /tmp/callback.js&& time node /tmp/promise.js
node /tmp/callback.js 0.39s user 0.87s system 163% cpu 0.776 total
node /tmp/promise.js 0.58s user 0.92s system 168% cpu 0.885 total
❯ time node /tmp/callback.js&& time node /tmp/promise.js
node /tmp/callback.js 0.36s user 0.93s system 166% cpu 0.777 total
node /tmp/promise.js 0.61s user 0.90s system 170% cpu 0.884 total


一个串行 一个并行?

为什么拿 IO 来测试,IO 时间本来就是不确定的。

再说,为什么你会有 callback 和 promise 性能问题,这两东西语法上是可以互转的,就像你那个 util.promisify 。

一般来说,node 会把所有事件处理完毕就退出,所以你的代码,最慢那次 IO 操作就是程序的运行时间。

第一个,你这是 typescript,tsconfig 里 target 用的是什么呢?如果不是 esnext 的话用 async/await 会有很多额外开销。

第二个,什么版本的 nodejs ? 14+就不要用 utils.promisfy 了,import fs from 'fs/promises’不好吗

async 和 forEach 不搭,改成

await Promise.all(files.map(async (file) => {}))) 试试看

哦,对了,你需要把两个 util.promisify 拿到 list_dir 外面来,这样测出的结果差距就小一些了:

❯ time node /tmp/promise.js && sleep 1 && time node /tmp/callback.js
node /tmp/promise.js 0.45s user 0.75s system 142% cpu 0.837 total
node /tmp/callback.js 0.58s user 0.99s system 196% cpu 0.800 total
❯ time node /tmp/promise.js && sleep 1 && time node /tmp/callback.js
node /tmp/promise.js 0.53s user 0.98s system 175% cpu 0.855 total
node /tmp/callback.js 0.38s user 1.00s system 173% cpu 0.794 total
❯ time node /tmp/promise.js && sleep 1 && time node /tmp/callback.js
node /tmp/promise.js 0.55s user 0.98s system 183% cpu 0.835 total
node /tmp/callback.js 0.52s user 0.86s system 173% cpu 0.797 total

❯ time node /tmp/callback.js && sleep 1 && time node /tmp/promise.js
node /tmp/callback.js 0.48s user 0.90s system 173% cpu 0.801 total
node /tmp/promise.js 0.63s user 1.02s system 192% cpu 0.857 total
❯ time node /tmp/callback.js && sleep 1 && time node /tmp/promise.js
node /tmp/callback.js 0.41s user 0.98s system 172% cpu 0.807 total
node /tmp/promise.js 0.59s user 0.89s system 172% cpu 0.857 total
❯ time node /tmp/callback.js && sleep 1 && time node /tmp/promise.js
node /tmp/callback.js 0.42s user 1.05s system 184% cpu 0.798 total
node /tmp/promise.js 0.56s user 0.95s system 177% cpu 0.848 total

await 性能非常非常差,注重性能的场合尽量回避。不过在 IO 的场合不明显,偏向纯计算的时候就非常明显了。

这里有个测试 https://gist.github.com/EtherDream/52649e4939008e149d0cb3a944c055b7

js<br>async function pending() {<br> return 11<br>}<br><br>function mayPending() {<br> if (Math.random() &lt; 0.001) {<br> return pending()<br> }<br> return 22<br>}<br><br>async function main() {<br> console.time('s1')<br> for (let i = 0; i &lt; 1e6; i++) {<br> const val = await mayPending()<br> }<br> console.timeEnd('s1')<br><br><br> console.time('s2')<br> for (let i = 0; i &lt; 1e6; i++) {<br> const ret = mayPending()<br> const val = ret instanceof Promise ? await ret : ret<br> }<br> console.timeEnd('s2')<br>}<br><br>main()<br>

s1: 1715.09521484375 ms
s2: 20.174072265625 ms

#7 这明显是你的问题了, 性能有差距也不会差这么多,你都差了 80 多倍了,而且引入随机数产生的不确定性更能被采纳为性能测试指标。

#8 更能 => 更不能

#7 https://onecompiler.com/javascript/3x5s8ss8k 在线跑差分的话,1e6 次性能差距也就是几毫秒,几乎是可以忽略的,因为你实际场景根本不会遇到这种超大量的 promise 混杂的情况,纯计算受影响的也只是线程切换,不会有这种开一大堆 promise 去计算的,这个是纯粹的开销,不符合实际场景的。

即使单次随机数耗时不固定,但在大量次数下是稳定的,你可以试试取消 await 部分,纯粹计算随机数两者用时几乎是相等的。事实上 await 就是差那么多,甚至不止 80 倍。可以看 js 引擎源码,await 实现开销很大的,比传统语言的纤程开销大很多。

开 promise 去计算的情况是有的,当然是代码写的不够好。比如有些函数 99.99% 情况只是计算,极小情况可能会用 await 调用一个异步 API,但 await 必须放在 async 函数里,所有每次调这个函数都是在创建 promise 。

可以试试这个:

console.time(‘s’)
for (let i = 0; i < 10000; i++) {
await i
}
console.timeEnd(‘s’)

执行 1 万次 await 耗时差不多 20ms 了。

#13 异步开销是有的没错,但是我更倾向于你的测试方式不切实际,用超短程测试方式当然会显著增加额外开销这是必然的,但是实际应用下几乎不可能会有人写这样的代码,首先你 await 对象就是 non-promise,没人会这么干,而且你这种场景是很容易被“编译器”优化的,用空跑来对比的方式其实是完全舍弃掉了 async/await 的优势,仅仅是单把额外开销拿出来放大了而已,你的代码越常规这点开销越不显眼的,不然这个测试方式不仅是 js,任何语言的优势都可以被测出来无限的额外开销。

我居然看不懂

await 一个 non-promise 是很常见的。比如一个 async 函数再调用 IO 之前先判断内存缓存,存在就直接返回了。但调用方仍然会用 await 去执行这个函数,相当于 await non-promise 。而且不知为什么目前所有 JS 引擎都不会优化这种调用,所以才有上述那个 gist 的测试案例。

为啥我觉得是 forEach+await 的问题,每次都停下来等了

  1. util.promisify 移到方法体外
    2. forEach + await 是谁教你的…

你等于在问
<br>async function foo() {}<br><br>await foo()<br>
不是即时执行的,这就是 js 把 async 和 promise 绑定带来的问题,一个 async 函数,无论怎样返回的东西都是一个 promise,所以一定要在下一个 microtask 里执行

async 和 forEach 不搭,这个没有考虑到,但是修改后,性能没有很大的改进
去除 util.promisify 差距也是蛮大的
目前看来就是 promise 的开销问题
https://imgur.com/daBglGj

async 和 forEach 一起写一般 IDE 会有提示吧。
不是很清楚,试试楼上的写法再测一下

我从 ES6 正式发布之后就一直在关注 V8 的 promise 性能情况,V8 官方有个测试用例 https://github.com/v8/promise-performance-tests
你可以 clone 跑一下看看,涵盖了原生 promise 、async/await 和主流的 promise 库的性能测试。我是见证了从一开始被 bluebird 虐逐渐到完爆 bluebird 。不过这个用例里面没有 callback,你可以参考其他的例子自己写一个。

这种测试最好不要引入 fs,因为文件系统是由操作系统管理调度的,有些操作系统比如 Linux 会有一些奇淫技巧的缓存优化,所以要测的话就测语法本身的计算性能,看语法糖是否会带来额外的计算性能损耗,或者是否反而提升了计算效率。

这是正确答案

重启电脑,先运行 callback.js 再运行 promise.js 试试,vfs cache~~

在Node.js中,callbackPromise是处理异步操作的两种常见方式。关于它们的性能差距,主要源于错误处理和代码可读性的不同,而不是直接的执行效率差异。然而,Promise在代码组织上更加优雅,并且配合async/await语法可以显著简化异步代码,这在一定程度上提升了开发效率和可维护性,间接影响了“性能”——即开发效率和代码质量。

以下是一个简单的对比示例:

使用Callback

function fetchDataCallback(callback) {
    setTimeout(() => {
        callback(null, 'data');
    }, 1000);
}

fetchDataCallback((err, data) => {
    if (err) throw err;
    console.log(data);
});

使用Promise

function fetchDataPromise() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve('data');
        }, 1000);
    });
}

fetchDataPromise().then(data => {
    console.log(data);
}).catch(err => {
    console.error(err);
});

在性能上,两者在执行异步任务时的时间消耗几乎相同,因为底层实现都依赖于事件循环。然而,Promise提供了更好的错误处理机制(通过.catch())和链式调用,使得代码更加清晰和易于维护。

对于更复杂的异步逻辑,尤其是涉及到多个异步操作串联或并行执行时,推荐使用Promise结合async/await,这将大大提高代码的可读性和可维护性。

回到顶部