【问题标题】:Is there a Performance Difference of Non-Parallel Async Loop vs Sync Loop in Node.js?Node.js 中的非并行异步循环与同步循环是否存在性能差异?
【发布时间】:2019-08-31 08:58:21
【问题描述】:

我将之前使用 async/await 同步到异步代码的 Node.js (v10.13.0) 的 JavaScript 代码重构了。后来我注意到的是程序执行时间慢了约 3 倍的性能下降。

将同步函数调用链转换为异步函数调用时是否会降低性能?

简化示例

更改同步码

function fn1() {  
   return 1;
}

function fn2() { 
   return fn1();
}

(function() {
  const result = fn2();
});

进入异步代码:

async function fn1() {  
   return 1;
}

async function fn2() { 
   return await fn1();
}

(async function() {
   const result = await fn2();
})();

是否有任何事件循环魔术可以使 Node.js webapp 中的后一个代码变慢?

【问题讨论】:

  • 所有这一切都是用 async 函数创建不必要的 Promise 并稍微改变执行顺序。您不会像这样使同步代码异步。
  • asyncawait 是语法糖。本机实现将生成大量额外代码(生成器函数),这些代码将跟踪异步操作完成时下一步要移动的位置。这会增加开销并降低性能。
  • @MarkMeyer 澄清用例:在我的场景中,必须在 fn1() 中实现异步数据库获取操作,并且 fn1() 的调用者必须等待返回结果。这就是为什么我必须以某种方式更改调用链中的函数,例如 fn2()。我不是想无缘无故地做一些异步的事情。 :) 传递回调函数会更有效吗?
  • @MartinLöper 如果您没有 await 执行任何操作,请删除 async 声明。 IE。如果fn(1) 是您的异步​​数据库调用,您可以省略调用并直接返回Promise,如果您不要必须在数据库调用返回后对结果执行任何操作。这意味着您可以将fn2() 更改为不是async 并直接返回fn1()。唯一需要 async 关键字的地方是,当你想从异步调用中获取结果时,那是在你的 IIFE 中。
  • 很明显,向函数添加不必要的开销确实会减慢它们的速度。这真的出乎意料吗?你为什么要“将之前同步的代码重构为异步代码”?

标签: javascript node.js performance async-await


【解决方案1】:

这是一个更高级的基准,它使用同步或异步函数计算斐波那契数列:

async function benchmark(M = 1000000, N = 100) {
    function fibonacci_sync(num) {
        let a = 1, b = 0, temp
        while (num >= 0) {
            temp = a; a = a + b; b = temp; num--
        }
        return b
    }
    async function fibonacci_async(num) {
        let a = 1, b = 0, temp
        while (num >= 0) {
            temp = a; a = a + b; b = temp; num--
        }
        return b
    }
    timeitSync  ('sync',  M,       () => {for(let i = 0; i < N; i++) fibonacci_sync(i)})
    await timeit('async', M, async () => {for(let i = 0; i < N; i++) await fibonacci_async(i)})
}

node.js 中的示例执行时间 - 异步结果慢了 2.8 倍

sync:    4.753s
async:  13.359s

使用更大的M,但更小的N = 10 而不是 N=100(更短的计算,所以 await 有更大的影响),异步函数变得 14.5 倍慢(哎呀!!) :

sync:   0.499s
async:  7.258s

这是在节点 v16.13.1 上。该基准的灵感来自这篇文章: https://madelinemiller.dev/blog/javascript-promise-overhead/

为了完整起见,这里是上面使用的timeit 函数:

async function timeit(label, repeat, fun) {
    console.time(label)
    for (let i = 0; i < repeat; i++) await fun()
    console.timeEnd(label)
}
function timeitSync(label, repeat, fun) {
    console.time(label)
    for (let i = 0; i < repeat; i++) fun()
    console.timeEnd(label)
}

当用async timeit而不是timeitSync测量fibonacci_sync时,执行时间从0.499s增长到最后一个示例中的1.2s,这再次证实async带来了很多减速。

所以,是的,确实,异步调用可能会导致性能大幅下降,甚至是一个数量级。每个调用都必须经过一个事件队列,其管理似乎会产生大量开销。在实现包含大量异步函数的代码时,绝对应该考虑到这一点。

考虑到async 范式的“感染性”——一个单一的低级函数是异步的,树上的所有调用者也必须是异步的——我很高兴 JS 引入了优化以允许 await...在某些(大多数)情况下会立即执行,而不是每隔几条指令就一次又一次地推送到队列中。这可以使await 包含在条件中并且很少需要实际停止函数但仍然需要将函数声明为“异步”的所有场景受益,无论多久达到“等待”。

【讨论】:

  • "每个调用都必须通过一个事件队列" - 这不是函数调用,而是在通过微任务队列的承诺解决后恢复 awaiting 代码。 “JS 引入了优化以允许 await... 在某些(大多数)情况下立即执行” - 不,它不会,也永远不会。 await 表达式保证异步(就像 .then() 调用它们是语法糖一样)是异步函数的一个重要特征。
  • the article that you linked 的关键要点:“这里最简单的解决方案是在应用程序的根附近执行数据获取或其他异步操作,并将结果数据向下传递。通常程序结构涉及深度嵌套的异步/等待路径表现出较差的关注点分离。理想情况下,单个系统不应该加载和使用数据;相反,它应该从另一个加载它的系统接收数据。这种结构还具有更多的额外好处可测试。"
  • @Bergi:在某些情况下,await 表达式保证异步性的事实...... 是一个错误,而不是一个特性 - 上面的基准测试说明了原因。程序员通常希望通过“等待”实现的是在特定位置允许,而不是强制异步。解释器和 JIT 编译器的工作应该是决定在给定时刻是否需要延迟执行——这种优化正是 JIT 编译器的用途。不,我不认为让程序员负责这些优化会帮助他们构建更好的软件。
  • 关于 JS 引擎应该或不应该做什么的有趣讨论,伙计们!当我扩展我们的代码库时,我完全不打算强制执行异步行为。我只是想为一个由于一些 for 循环而被调用了十几次的深层嵌套方法允许异步。
猜你喜欢
  • 1970-01-01
  • 2015-02-27
  • 1970-01-01
  • 2018-10-17
  • 2015-04-11
  • 1970-01-01
  • 2018-09-18
  • 2014-02-06
  • 1970-01-01
相关资源
最近更新 更多