async介绍
异步函数是一个使用隐式Promise异步操作以返回其结果的函数。异步函数旨在使异步代码看起来像同步代码,为开发者隐藏异步处理的一些复杂性,使开发者用起来语法简洁清晰,可以直接用trycatch进行异常处理。但是在我们关注于异步函数开发调试遍历方面时,我们对他的性能演进可能并不是那么关注,本文我们就来介绍下V8对于异步函数的性能优化过程。
async每个node版本的数据
上图是doxbee benchmark,它评估了Promise的性能。请注意,图表中的执行时间越低意味着性能越好。V8已经在v5.5到v6.8之前明显的提升了异步代码的性能,接下来我们就来介绍下V8是如何优化异步函数的。
提升的原因
总的来说,性能提升主要得益于以下三项
-
新的优化编译器TurboFan
-
新的垃圾收集器Orinoco
-
以及从Nodejs8的await BUG中得到灵感,诞生的第三个优化点
const p = Promise.resolve();
(async () => {
await p; console.log('after:await');
})();
p.then(() => console.log('tick:a'))
.then(() => console.log('tick:b'));
本文重点讲下第三点,首先大家看下上述代码,上面的程序创建了一个状态为fulfilled的Promise,然后await取得它的结果,与此同时也将后面的 2 个then函数处理程序链接到它上面,正常思维的话既然p的状态已经是fulfilled了,你可能会认为首先打印 'after:await' 然后再打印 'tick',实际上node8下确实是这样表现的
但是当我们把node版本切换到10,在运行这段脚本的时候,得到的确实这个结果
为什么不同版本得出来的结果会截然不同呢,原来在node8中,虽然行为看起来合理,但它其实是Node.js 8这个版本中的BUG,根据规范它并不正确,Node.js 10实现了正确的行为,即首先执行链式处理程序,然后继续使用异步函数。
接下来让我们看下V8引擎底层是如何实现await规范的,这是一个简单的异步函数foo
async function foo(v) {
const w = await v;
return w;
}
看下引擎底层是如何处理此函数的
1. V8 将此函数标记为可恢复(这意味着可以暂停执行并稍后恢复执行)
2. 创建implicit_promise(这是在调用异步函数时返回的Promise,并最终解析(resolve)为异步函数生成的值)
3. 将v转换为Promise- v代表传递给await的值
4. 给Promise附加处理程序以便稍后恢复异步函数
5. 挂起异步函数并返回implicit_promise给调用者
6. promise变为fulfilled,恢复异步函数的执行,并将promise的值赋值给w(implicit_promise被resolved后的值)
关注下第3个步骤,无论v是否是一个promise,都会重新创建一个promise包装器,然后立即用await解析包装器v的值,如果v本身不是promise的话,当然是没有问题的,但是多数情况下我们在使用await后面传入的都会是一个promise,那么就造成了额外的开销。
可以用图中方式,避免额外开销,此操作返回没有修改过的promise,并且只在必要时将其值包装到promise中。当传递给await的值已经是一个Promise时,这可以节省其中一个额外的promise,加上microtick队列上的两个tick。并且这种方式不仅可以提升性能,并且对于上文提到的日志打印顺序,也可以和Node.js 8版本一致。
但这次并不是BUG,它现在是一个正在标准化的优化。已经在nodejs12中开启了。
文章评论