ecmascript赋值-做好准备:新的 V8 已发布,Node 性能正在发生变化

2023-08-26 0 5,122 百度已收录

V8涡扇发动机的性能特征如何影响我们的优化方法

自从node.js诞生以来,依靠V8 javascript引擎为我们的代码提供执行环境,我们使用我们熟悉和喜爱的编程语言。 V8 javascript引擎是Google为Chrome浏览器编译的虚拟机。

从一开始,V8 的主要目标就是让 JavaScript 执行速度更快,至少比其竞争对手更快。 在高度动态的弱类型语言中,这个目标并不容易实现。 本文介绍的是 V8 和 javascript 引擎的性能演变。

V8引擎的核心部分允许使用JIT(即时编译)编译器高速执行javascript,JIT编译器是一种在运行时优化代码的动态编译器。 当V8第一次构建JIT编译器时,它被命名为:CrankShaft。

自 20 世纪 90 年代以来,在外行和 javascript 用户看来,javascript 执行的快慢是不可预测的ecmascript赋值,并且很难完全理解 javascript 执行缓慢的原因。

近年来,Matteo Collina 和我一直致力于研究如何编写高性能的 Node.js 代码,这当然意味着他们知道如何让 V8 快速或慢速地执行我们的代码。

现在,V8团队已经编写了一个新的JIT编译器:Turbofan。 现在轮到我们挑战我们所有关于性能的假设了。

从更常见的 V8 杀手(一段导致优化平坦的代码,这个术语在涡轮风扇上下文中不再有意义)到不太常见的 Matteo,围绕曲轴性能,我们将通过一系列微观来了解 V8 版本- 基准结果性能差异。

当然,在优化V8逻辑路径之前,首先要关注API设计、算法、数据结构。

这些微基准是 javascript 如何跨节点版本更改执行的指标。 在我们已经应用了传统的优化方法之后,我们可以使用这个指标来改进我们的总体编码风格并提高程序的性能。

我们将使用V8版本5.1、5.8、5.9、6.0、6.1来观察该微指标的性能。

将此版本放在以下环境中:带有 V8 5.1 和 Crankshaft JIT 编译器的 Node 6,带有 V8 5.8、Crankshaft 和 Turbofan 混合 JIT 编译器的 Node 8.0 到 8.2。

到目前为止,V8 5.9 或 6.0 将出现在 Node 8.3(也可能是 Node 8.4)中,6.1 是集成在实验性 Node-v8 存储库中的最新版本的 V8。 换句话说,V8 6.1 版本将集成到未来的 Node 版本中。

让我们一起看看我们的微观参考点,另一方面,我们将讨论这些参考点对未来意味着什么。

尝试/捕获问题

最著名的反优化模式之一是使用“try/catch”代码块。

在此参考点中,我们比较四种场景的案例:

代码地址:

我们可以看到,在Node 6(V8 5.1)中,“try/catch”带来的性能问题是真实存在的,但在Node 8.0-8.2(V8 5.8)中,对性能的影响明显增加。

另请注意,在 Node 6 (V8 5.1) 和 Node 8.0-8.2 (V8 5.8) 中,在 try 块内调用函数的执行速度比在 try 块外调用函数慢得多。

但是,对于 Node 8.3+,在“try”块内调用函数的性能问题可以忽略不计。

不过,也不要太掉以轻心。 在处理一些演练研讨会材料时,Matteo 和我发现了一个性能错误,其中相当特定的情况组合可能会导致 Turbofan 的无限优化/重新优化循环(这将被视为“杀手” - 一种破坏性的性能模式)。

删除对象的属性

多年来,删除仅限于任何希望编写高性能 JavaScript 的人(至少我们正在努力为流行程序编写最好的代码)。

“删除”问题归结为 V8 处理 JavaScript 对象和(可能是动态的)原型链的动态特性,使得属性查找在实现层面变得越来越复杂。

V8引擎高性能创建对象属性技术是在C++层根据对象的“形状”创建类。 形状本质上是通配符属性(包括原型链键和值)。

这些被称为“隐藏类”。 然而,这是在运行时发生在对象上的优化,如果对象的形状存在不确定性,V8 还有另一种属性检索模式:哈希表查找。

哈希表查找速度明显慢一些。 以前,当我们从对象中删除键时,后续的属性访问将是哈希表查找。 这就是为什么我们阻止删除,而是将属性设置为 undefined,到目前为止,两者具有相同的值,但后者在检测属性是否存在时可能会出现问题; 由于 JSON.stringify 在其输出中不包含“未定义”值(“未定义”在 JSON 规范中不是有效值),因此预序列化编译通常足够好。

现在,让我们看看新的 Turbofan 实现是否解决了删除问题。

在此微基准测试中,我们比较两种情况:

代码地址:

在 V8 6.1(尚未在任何 Node 版本中使用)中,操作对象上已删除的属性非常快,甚至比设置为“未定义”还要快。 这是个好消息,因为现在我们可以使用删除,而且删除速度更快。

泄漏和整理“争论”

常见的 JavaScript 函数(箭头函数除外,它们内部没有参数对象)可以使用类字段的隐式“arguments”对象。

为了使用链表方法或大多数链表行为,“arguments”对象的索引属性已被复制到链表中。 过去,JavaScript 程序员长期以来一直倾向于认为代码越少执行速度越快。

虽然这条经验法则可以为浏览器端代码带来有效负载大小的好处,但同样的规则可能会给服务器端带来麻烦,因为在服务器端,代码大小远没有执行速度那么重要。

因此,将arguments对象转换为链表的方法非常流行:Array.prototype.slice.call(arguments),调用链表的slice函数并将arguments对象作为函数的this上下文传递,并且slice 函数看到一个像链表这样的对象,并相应地采取行动。 也就是说,它将整个类字段的arguments对象视为一个链表。

但是,当函数的隐式参数对象从函数上下文中公开时(例如,当它从函数返回或传递给另一个函数时,如 Array.prototype.slice.call(arguments) 的情况,下同)导致性能提高。 现在是挑战这一假设的时候了。

下一个微基准检查我们的四个 V8 版本中的两个相互关联的主题:泄漏“参数”的成本和将参数复制到链接列表中的成本(随后从函数范围公开以替换“参数”对象)。

详情如下所示:

代码访问地址:

让我们看一下相同的数据,绘制为线图以指示性能特征的变化:

总结一下:如果我们想使用链表作为处理函数的输入(在我的经验中似乎相当常见),那么在 Node 8.3 或更高版本中我们应该使用扩展运算符。

在 Node 8.2 或更低版本中,我们应该使用 for 循环将参数中的键复制到新的(预分配的)数组中(有关详细信息,请参阅基准代码)。

此外,在 Node 8.3+ 中,我们不会因为将arguments参数暴露给其他函数而遇到性能问题,因此在不需要完整链表并且可以使用类似链表的结构的情况下可能会有进一步的性能优势。

部分应用(CURRYING)和绑定

部分应用(或柯里化)是指我们可以捕获嵌套闭包内状态的方法。

例如:

function add (a, b) {
  return a + b
}
const add10 = function (n) {
  return add(10, n)
}
console.log(add10(20))

这里add函数的参数a在add10函数中,形参为10。

从 EcmaScript 5 开始,使用“bind”方法提供了一种更简单的部分应用编写形式:

function add (a, b) {
  return a + b
}
const add10 = add.bind(null, 10)
console.log(add10(20))

然而,我们通常不使用bind,因为它比使用闭包慢得多。

该基准测试检测我们的目标 V8 版本的绑定和闭包之间的差异,直接调用该函数。

以下是我们的四个案例:

代码访问地址:

基准测试结果的线图清楚地说明了新 V8 版本中发生的聚合。 有趣的是,一些使用箭头函数的应用程序比使用普通函数要快得多(至少在我们的微基准测试案例中)。 事实上,它的性能几乎与直接调用一样。

在 V8 5.1(Node 6)和 5.8(Node 8.0-8.2)中,绑定非常慢,并且显然使用箭头进行部分应用是最快的选项。 然而,“绑定”速度从 V8 版本 5.9(Node 8.3+)开始增加,并且在 V8 版本 6.1(未来的 Node)中最快。

在所有版本中,使用箭头函数是最接近直接调用的。 在以前的新版本中使用箭头函数的代码将与使用“bind”一样接近,并且目前比使用普通函数更快。

然而,需要注意的是,我们可能需要使用不同大小的数据结构来测试更多类型的部分应用程序,以获得更全面的了解。

功能字符数

函数的大小与函数名、空格甚至注释有关,无论V8是否可以将函数处理为一行。 是的:向函数添加注释可能会导致性能提高 10% 范围内。 这种现象在涡扇发动机上会改变吗? 让我们来看看。

在此基准测试中,我们考虑三种情况。

代码访问地址:

在 V8 5.1 (Node6) 中,小函数和内联函数以相同的速率执行。 这充分展示了内联函数是如何工作的。 当我们调用这个小函数时,就像V8将小函数的内容写入到它调用的地方。

因此,当我们实际编写函数的内容时(即使有额外的注释填充),我们会自动使函数内联,并且性能是相同的。

同样,我们可以在 V8 5.1 (Node6) 中看到,调用填充了超过一定大小的注释的函数会导致执行速度变慢。

在 Node 8.0-8.2 (V8 5.8) 中,情况几乎相同,只是调用小函数的成本要低得多; 这可能是由于 Crankshaft 和 Turbofa 一起工作,而某个功能可能位于 Crankshaft 中。

另一个可能导致 Turbofan 中内联功能分离的特性(即一系列内联函数簇之间必须有跳转)。

在 5.9 及更高版本(Node 8.3+)中,由不相关字符(例如空格或注释)添加的任何大小与函数性能无关。

这是因为 Turbofan 使用函数 AST(抽象语法树)来确定函数大小,而不是 Crankshaft 中的字符数。

它不检测字节数,而是考虑函数的实际指令,因此从 V8 5.9 (Node 8.3+) 开始,**空格、变量名子字符计数、函数签名和注释不再是函数被调用的诱因。内联。

值得注意的是,我们再次看到功能的整体性能提高。

这里总结的应该也是编译小函数。 目前我们仍然需要防止函数内部出现过多的注释(甚至是空格)。

此外,如果您想要绝对最快的速度,手动内联(删除调用)始终是最快的方法。 当然,这必须与以下事实相平衡:在达到一定大小(实际可执行代码)后,函数将不会被内联,因此将代码从其他函数复制到函数中可能会导致性能问题。

换句话说,手动内联是一种可选的优化; 在大多数情况下,最好让编译器处理内联代码。

32 位与 64 位整数

我们都知道 JavaScript 只有一种数字类型:Number。 (也许这里应该包括一句关于 BigInt 动议的句子?)

然而,V8 是用 C++ 实现的,因此必须在幕后选择数字类型的 JavaScript 值。

对于整数(也就是我们在 JS 中指定不带小数的整数),V8 假定所有数字都是 32 位 - 直到它们不是。

这实际上是一个公平的选择,因为在许多情况下数字的范围是 0-65535。 如果 JavaScript(完整)数字超过 65535,JIT 编译器必须动态地将数字的基础类型更改为 64 位 - 这也可能会影响其他优化。

该基准测试考虑以下三种情况:

代码访问地址:

从图中可以看出,无论是 Node 6 (V8 5.1) 还是 Node 8 (V8 5.8),甚至是 Node.js 的未来版本,这一观察是否成立。 大于 65535 的数字(整数)将导致函数以一半到三分之二的速率运行。 因此,如果您有很长的数字 ID,请将它们放入字符串中。

值得注意的是,32 位范围内的数字在 Node 6 (V8 5.1) 和 Node 8.1 和 8.2 (V8 5.8) 之间也显着减慢,但在 Node 8.3+ (V8 5.9+) 中显着减慢。

由于大数字根本不会影响速率,因此这很可能是实际(32 位)数字处理速度减慢的原因,而不是与函数调用或循环速率(在基准代码中使用)有关。

遍历对象

获取对象的所有值(或属性)并使用这些数据执行一些操作是一项常见任务,并且有很多方法可以解决这个问题。 让我们看看哪个版本的 V8(和 Node)最快。

该基准测试在所有 V8 版本中检测到四种情况:

1)、使用for-in循环和hasOwnProperty检测来获取一个对象的所有值

2)、使用Object.keys并使用Arrayreduce来迭代keys,在提供的iterator函数中访问对象的属性值来reduce

3)使用Object.keys并使用Arrayreduce迭代key,并在reduce提供的迭代器箭头函数中访问对象的属性值

4)使用for循环Object.keys返回的字段来访问循环内部对象的属性值

我们还删除了 V8 5.8、5.9、6.0 和 6.1 的另外三个案例

1)、使用Object.values并使用链表的reduce方法来迭代value

2)、使用Object.values并使用链表的reduce方法来迭代value,传递给reduce的函数是一个箭头函数

3)使用for循环返回Object.values的字段

我们不在 V8 5.1 (Node 6) 中测试这些案例,因为它不支持本机 EcmaScript 2015Object.values 方法。

我们不会在 V8 5.1(节点 6)中测试这种情况,因为它不支持本机 EcmaScript 2015Object.values 技巧。

代码访问地址:

在 Node 6 (V8 5.1) 和 Node 8.0-8.2 (V8 5.8) 中使用 for-in 是迄今为止循环对象的所有键,然后在循环内访问对象的值的最快方法。 每秒大约4000万次操作,比最接近的方法快5倍,Object.keys大约800万次。

ecmascript赋值-做好准备:新的 V8 已发布,Node 性能正在发生变化

在V8 6.0(Node 8.3)中,之前版本的for-in将速率降低了四分之一,但它仍然比任何其他方式都要快。

在V8 6.1(Node的未来版本)中,Object.keys的速度会飞起来,看起来会比使用for-in更快,但是在V8 5.1中它不如V8 5.1和5.8(Node 6、节点8.0-8.2)换入率。

也许 Turbofan 背后的驱动原理是对直观编码行为的优化。 也就是说,针对开发人员最符合人体工程学的情况进行了优化。

直接使用Object.values获取值比使用Object.keys并访问对象中的值要慢。 除此之外,处理循环比函数式编程更快。 因此,迭代对象时可能需要做更多的工作。

另外,对于那些使用 for-in 来提高性能的人来说,当我们大幅增长并且无法替代它时ecmascript赋值,这将是一个痛苦的时刻。

创建对象

我们一直在创建对象,所以这是一个很好的检查点。

我们将看看以下三种情况:

1)使用对象字面量创建对象

2)、使用ES2015类创建对象

3)、使用构造函数创建对象

代码仓库地址:

在 Node 6 (V8 5.1) 中,所有方法的速度大致相同。

在 Node 8.0-8.2 (V8 5.8) 中,使用 EcmaScript 2015 类创建实例的速度不到使用构造函数的对象字面量的一半。 这里应该非常小心。

V8 5.9 中仍然如此。

然后在 V8 6.0(可能在 Node 8.3,或者可能在 Node 8.4)和 6.1(目前不在任何 Node 版本中)中,对象创建率令人难以置信地超过 5 亿次操作/秒! 这太不可思议了。

我们可以看到构造函数创建的对象稍微慢一些。 因此,我们对未来友好的执行代码的最佳准备总是倾向于对象文字。 这适合我们,因为我们建议从函数返回对象文字(而不是使用类或构造函数)作为一般最佳编码实践。

多态和单态函数

当我们总是将相同类型的参数传递给函数时(假设我们总是传递一个字符串),我们以单态方式使用该函数。

有些函数是用多种类型的参数编写的 - 这意味着相同的参数可以被视为不同的隐藏类 - 因此实际上它可以处理字符串、链表或具有特定隐藏类的对象并相应地处理它。 这可以使套接字在个别情况下表现良好,但会对性能产生负面影响。

让我们看看我们的基准测试中多类型和单一类型的情况是什么样的。

我们来看五种情况:

代码仓库地址:

图中显示的数据最终证明,在所有 V8 版本中,单态函数的性能均优于多态函数。

V8 6.1(未来的节点版本)中单态函数和多态函数之间更大的性能差异进一步支持了这一点。 然而,值得注意的是,这是基于使用 Node-v8 分支的 nightly-build V8 版本 - 它可能不会在 V8 6.1 中具体实现。

如果我们正在编写需要优化的代码,一个函数会被多次调用,那么我们应该避免使用多态性。

另一方面,对于实例化/设置函数,如果仅调用一次或两次,则多态 API 的性能是可以接受的。

调试器关键字

最后,我们来谈谈 debugger 关键字

确保从代码中删除调试器语句。 代码中的调试器语句将提高性能。

我们来看两个案例:

代码仓库地址:

在所有测试的V8版本中,debugger关键字的存在对性能有着可怕的影响。

随着连续的 V8 版本的推出,不带调试器的产品线显着增长,我们将在摘要中对此进行描述。

真实世界基准:记录仪比较

除了我们的微基准之外,我们还可以使用最流行的 Node.js 记录器 Matteo 来观察 V8 版本的整体效果。

代码地址:.

下面的条形图代表了 Node.js 5.9 (Crankshaft) 中最流行的记录器的性能:

以下是使用 V8 6.1 (Turbofan) 的相同基准测试:

虽然所有记录器基准测试都获得了大约 2 倍的加速,但 Winston 记录器从新的 Turbofan JIT 编译器中获得了最大的收益。 这可能表明我们在微基准测试中看到的各种模式的速度收敛:曲轴中的较慢模式在涡轮风扇中明显更快,而曲轴中的快速模式在涡轮风扇中明显慢。

最慢的温斯顿可能使用较慢的曲轴形式,但在涡轮风扇中速度更快,而皮诺则经过优化以使用最快的曲轴形式。 虽然皮诺的利率有所下降,但幅度要小得多。

总结

一些基准测试表明,在完全启用 Turbofan 的情况下,V8 5.1、V8 5.8 和 5.9 中的扁平情况在 V8 6.0 和 V8 6.1 中变得更快,较快的情况显得较慢,通常较慢的情况会变得较快。

其中大部分是因为在 Turbofan(V8 6.0 及更高版本)中进行函数调用的成本。 Turbofan 背后的想法是优化常见情况并淘汰常用的“V8 杀手”。 这可以在浏览器 (Chrome) 和服务器 (Node) 应用程序中实现更好的性能。

不过,权衡是(至少在最初)最有效情况下的速率增加。 我们的记录器基准比较表明,Turbofan 能够全面提升性能,甚至跨平台代码比较(例如 Winston 与 Pino)。

如果您已经关注 JavaScript 性能一段时间,并根据底层引擎的怪癖调整编码行为,那么差不多是时候忘记一些技术了。 如果您专注于最佳实践,编写了总体良好的 JavaScript,那么由于 V8 团队的不懈努力,您做得很好,性能奖励即将到来。

如果您一段时间以来一直担心 JavaScript 性能,并根据底层引擎的怪癖调整编码行为,那么是时候忘记一些方法了。 如果您专注于最佳实践,那么编写良好的代码通常会相当不错,并且由于 V8 团队的不懈努力,性能改进正式到来。

本文的所有源代码和另一个副本均可在此处获取。 本文的原始数据可在以下位置获取:

收藏 (0) 打赏

感谢您的支持,我会继续努力的!

打开微信/支付宝扫一扫,即可进行扫码打赏哦,分享从这里开始,精彩与您同在
点赞 (0)

悟空资源网 ecmascript ecmascript赋值-做好准备:新的 V8 已发布,Node 性能正在发生变化 https://www.wkzy.net/game/164300.html

常见问题

相关文章

官方客服团队

为您解决烦忧 - 24小时在线 专业服务