V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
kebyn
V2EX  ›  Node.js

callback 和 promise 性能差距疑问

  •  1
     
  •   kebyn · 134 天前 · 3731 次点击
    这是一个创建于 134 天前的主题,其中的信息可能已经有所发展或是发生改变。

    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
    

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

    24 条回复    2021-08-03 17:57:00 +08:00
    maokai
        1
    maokai   134 天前
    ❯ 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
        2
    raptium   134 天前 via iPhone
    一个串行 一个并行?
    muzuiget
        3
    muzuiget   134 天前
    为什么拿 IO 来测试,IO 时间本来就是不确定的。

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

    一般来说,node 会把所有事件处理完毕就退出,所以你的代码,最慢那次 IO 操作就是程序的运行时间。
    littlepanzh
        4
    littlepanzh   134 天前 via iPhone
    第一个,你这是 typescript,tsconfig 里 target 用的是什么呢?如果不是 esnext 的话用 async/await 会有很多额外开销。

    第二个,什么版本的 nodejs ? 14+就不要用 utils.promisfy 了,import fs from 'fs/promises'不好吗
    seki
        5
    seki   134 天前
    async 和 forEach 不搭,改成

    await Promise.all(files.map(async (file) => {}))) 试试看
    maokai
        6
    maokai   134 天前
    哦,对了,你需要把两个 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
        7
    myqoo   132 天前
    await 性能非常非常差,注重性能的场合尽量回避。不过在 IO 的场合不明显,偏向纯计算的时候就非常明显了。

    这里有个测试

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

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

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

    await foo()
    ```
    不是即时执行的,这就是 js 把 async 和 promise 绑定带来的问题,一个 async 函数,无论怎样返回的东西都是一个 promise,所以一定要在下一个 microtask 里执行
    kebyn
        20
    kebyn   131 天前
    @seki async 和 forEach 不搭,这个没有考虑到,但是修改后,性能没有很大的改进
    @maokai 去除 util.promisify 差距也是蛮大的
    @myqoo 目前看来就是 promise 的开销问题
    https://imgur.com/daBglGj
    CokeMine
        21
    CokeMine   131 天前
    async 和 forEach 一起写一般 IDE 会有提示吧。
    不是很清楚,试试楼上的写法再测一下
    libook
        22
    libook   131 天前
    我从 ES6 正式发布之后就一直在关注 V8 的 promise 性能情况,V8 官方有个测试用例 https://github.com/v8/promise-performance-tests
    你可以 clone 跑一下看看,涵盖了原生 promise 、async/await 和主流的 promise 库的性能测试。我是见证了从一开始被 bluebird 虐逐渐到完爆 bluebird 。不过这个用例里面没有 callback,你可以参考其他的例子自己写一个。

    这种测试最好不要引入 fs,因为文件系统是由操作系统管理调度的,有些操作系统比如 Linux 会有一些奇淫技巧的缓存优化,所以要测的话就测语法本身的计算性能,看语法糖是否会带来额外的计算性能损耗,或者是否反而提升了计算效率。
    TomVista
        23
    TomVista   130 天前
    @Austaras 这是正确答案
    hungrybirder
        24
    hungrybirder   116 天前
    重启电脑,先运行 callback.js 再运行 promise.js 试试,vfs cache~~
    关于   ·   帮助文档   ·   API   ·   FAQ   ·   我们的愿景   ·   广告投放   ·   感谢   ·   实用小工具   ·   2188 人在线   最高记录 5497   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 21ms · UTC 15:27 · PVG 23:27 · LAX 07:27 · JFK 10:27
    ♥ Do have faith in what you're doing.