callback 和 promise 性能差距疑问

2021-07-16 20:27:14 +08:00
 kebyn

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

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

5463 次点击
所在节点    Node.js
24 条回复
maokai
2021-07-16 20:48:19 +08:00
❯ 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
raptium
2021-07-16 20:52:25 +08:00
一个串行 一个并行?
muzuiget
2021-07-16 20:54:00 +08:00
为什么拿 IO 来测试,IO 时间本来就是不确定的。

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

一般来说,node 会把所有事件处理完毕就退出,所以你的代码,最慢那次 IO 操作就是程序的运行时间。
littlepanzh
2021-07-16 20:56:11 +08:00
第一个,你这是 typescript,tsconfig 里 target 用的是什么呢?如果不是 esnext 的话用 async/await 会有很多额外开销。

第二个,什么版本的 nodejs ? 14+就不要用 utils.promisfy 了,import fs from 'fs/promises'不好吗
seki
2021-07-16 21:01:53 +08:00
async 和 forEach 不搭,改成

await Promise.all(files.map(async (file) => {}))) 试试看
maokai
2021-07-16 21:06:28 +08:00
哦,对了,你需要把两个 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
myqoo
2021-07-17 23:57:17 +08:00
await 性能非常非常差,注重性能的场合尽量回避。不过在 IO 的场合不明显,偏向纯计算的时候就非常明显了。

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

```js
async function pending() {
return 11
}

function mayPending() {
if (Math.random() < 0.001) {
return pending()
}
return 22
}

async function main() {
console.time('s1')
for (let i = 0; i < 1e6; i++) {
const val = await mayPending()
}
console.timeEnd('s1')


console.time('s2')
for (let i = 0; i < 1e6; i++) {
const ret = mayPending()
const val = ret instanceof Promise ? await ret : ret
}
console.timeEnd('s2')
}

main()
```

s1: 1715.09521484375 ms
s2: 20.174072265625 ms
Mitt
2021-07-18 09:21:43 +08:00
@myqoo #7 这明显是你的问题了, 性能有差距也不会差这么多,你都差了 80 多倍了,而且引入随机数产生的不确定性更能被采纳为性能测试指标。
Mitt
2021-07-18 09:21:58 +08:00
@Mitt #8 更能 => 更不能
Mitt
2021-07-18 09:26:59 +08:00
@myqoo #7 https://onecompiler.com/javascript/3x5s8ss8k 在线跑差分的话,1e6 次性能差距也就是几毫秒,几乎是可以忽略的,因为你实际场景根本不会遇到这种超大量的 promise 混杂的情况,纯计算受影响的也只是线程切换,不会有这种开一大堆 promise 去计算的,这个是纯粹的开销,不符合实际场景的。
myqoo
2021-07-18 10:43:54 +08:00
@Mitt 即使单次随机数耗时不固定,但在大量次数下是稳定的,你可以试试取消 await 部分,纯粹计算随机数两者用时几乎是相等的。事实上 await 就是差那么多,甚至不止 80 倍。可以看 js 引擎源码,await 实现开销很大的,比传统语言的纤程开销大很多。
myqoo
2021-07-18 10:48:59 +08:00
@Mitt 开 promise 去计算的情况是有的,当然是代码写的不够好。比如有些函数 99.99% 情况只是计算,极小情况可能会用 await 调用一个异步 API,但 await 必须放在 async 函数里,所有每次调这个函数都是在创建 promise 。
myqoo
2021-07-18 10:57:12 +08:00
@Mitt 可以试试这个:

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

执行 1 万次 await 耗时差不多 20ms 了。
Mitt
2021-07-18 13:37:39 +08:00
@myqoo #13 异步开销是有的没错,但是我更倾向于你的测试方式不切实际,用超短程测试方式当然会显著增加额外开销这是必然的,但是实际应用下几乎不可能会有人写这样的代码,首先你 await 对象就是 non-promise,没人会这么干,而且你这种场景是很容易被“编译器”优化的,用空跑来对比的方式其实是完全舍弃掉了 async/await 的优势,仅仅是单把额外开销拿出来放大了而已,你的代码越常规这点开销越不显眼的,不然这个测试方式不仅是 js,任何语言的优势都可以被测出来无限的额外开销。
BaiLinfeng
2021-07-18 14:11:31 +08:00
我居然看不懂
myqoo
2021-07-18 14:32:01 +08:00
@Mitt await 一个 non-promise 是很常见的。比如一个 async 函数再调用 IO 之前先判断内存缓存,存在就直接返回了。但调用方仍然会用 await 去执行这个函数,相当于 await non-promise 。而且不知为什么目前所有 JS 引擎都不会优化这种调用,所以才有上述那个 gist 的测试案例。
dfkjgklfdjg
2021-07-19 09:45:07 +08:00
为啥我觉得是 forEach+await 的问题,每次都停下来等了
zhuweiyou
2021-07-19 10:24:27 +08:00
1. util.promisify 移到方法体外
2. forEach + await 是谁教你的...
Austaras
2021-07-19 14:55:13 +08:00
@myqoo 你等于在问
```
async function foo() {}

await foo()
```
不是即时执行的,这就是 js 把 async 和 promise 绑定带来的问题,一个 async 函数,无论怎样返回的东西都是一个 promise,所以一定要在下一个 microtask 里执行
kebyn
2021-07-19 15:00:52 +08:00
@seki async 和 forEach 不搭,这个没有考虑到,但是修改后,性能没有很大的改进
@maokai 去除 util.promisify 差距也是蛮大的
@myqoo 目前看来就是 promise 的开销问题
https://imgur.com/daBglGj

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/789987

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX