欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

[每周一译]为何新的V8引擎如此的快

程序员文章站 2022-03-07 17:06:00
...

Node.js社区中的许多人都很高兴看到V8的最新更新,这次更新包括V8的编译器体系结构以及大部分的垃圾收集器。TurboFan取代了Crankshaft,Orinoco采用并行机制进行垃圾回收,以及其他应用的优化。

第8版的Node.js附带了这个新改进的V8引擎,这意味着我们可以编写惯用的声明式JavaScript,而不必担心由于编译器的缺点而导致性能开销。V8团队也对这点进行了说明。

由于工作中应用到NodeSource ,所以我顺带研究了下V8的这些最新的变化,包括查阅V8团队发布的博客文章,阅读V8的源代码以及构建工具来验证特性的性能指标。

我把这些发现都整理到github的v8-perf项目里,以方便大家查看。这些资料也是本周我将在NodeSummit上发表演讲以及我的一系列博客文章的素材。

由于这次升级带来的变化多且复杂,我怕计划在本文里先提供一个简单介绍,然后在本系列的后续博客里继续探讨这个主题。

更多资料,请直接访问v8-perf项目

新的V8编译器管道

众所周知,先前的V8版本遭遇了所谓的优化杀手危机,而且这似乎无法再引擎中修复。V8团队也很难实现具有良好性能特征的新JavaScript语言功能。

其主要原因是V8的架构以及变得非常难以更改和扩展。优化编译器Crankshaft并没有考虑用一种可持续发展的语言来实现,而且编译器管道中层与层之间缺乏隔离也成为了一个问题。在某些极端情况下,开发人员必须为这四个基础体系结构手工编写汇编代码。

V8团队意识到这不是一个可持续发展的系统,特别是随着JS本身的发展速度加快,它也需要添加许多新的语言特性。因此他们便重新设计了一种新的编译器架构。这个新的编译器体系结构被划分为三个分离清晰的层,即前端层,优化层和后端层。

前端层主要负责生成由Ignition解释器运行的字节码,而优化层通过TurboFan优化编译器提高代码的性能。后端则执行较低级别的任务,如机器级优化,调度和为受支持的体系结构生成机器代码等。

仅后端的分离导致架构特定代码减少约29%,尽管新架构可以支持9个体系结构。

更小的性能抖动

这个新的V8架构的主要目标包括:
- 减小性能抖动
- 提高启动速度
- 改进基线性能
- 减少内存使用
- 支持新的语言特性

前三个目标与Ignition解释器的实现有关,第三个目标也通过该部分的改进实现了一部分。

首先,我将重点关注架构的这一部分,并结合上面提到的这些目标对其进行解释。

在过去,V8团队专注于优化代码的性能,而忽略了解释字节码的性能,这造成了剧烈的性能抖动,使得应用程序的运行时特性总体上难以预测。某个应用程序在运行正常的情况下,假如代码中某个地方触发了Crankshaft,引起它的优化进程,这将会导致巨大的性能坍塌——甚至在某些情况下,部分执行速度会慢100倍。为了避免这种情况,开发人员采用了编写Crankshaft 脚本进行编译器优化的方法进行改进。

然而结果显示,对于大多数的web页面来说,优化编译器并没有优化解释器重要,因为代码需要快速运行,没有时间来等待代码加载,而且由于投机性优化代价不小,优化编译器在某种情况下甚至会影响性能。

解决方案是改进字节码的基线性能。这是通过在生成时将字节码传递到内联优化阶段来实现的,从而产生高度优化的小型解释器代码,该代码可以执行指令并以低开销方式与V8 VM的其余部分进行交互。

由于字节码很小,内存使用也减小了,而且运行速度非常快,因此进一步的优化可以暂缓一下了。另外在尝试优化之前,可以通过内联缓存收集更多的信息,从而减少由于对代码执行方式的假设,而导致的去优化和重新优化的成本。

采用运行字节码的方式,而不是用TurboFan 来优化代码,就不会产生以前的有害影响了,因为它的性能更接近最优的代码;这意味着任何性能抖动都要小得多。

确保你的代码运行在最佳性能下

在使用新的V8时,大多数情况下你只要考虑编写声明式的JavaScript和使用优秀的数据结构和算法就可以了。但是,在应用程序的热代码运行时,你可能希望确保它能在最佳性能下运行。

TurboFan优化编译器使用先进技术使得热代码尽可能快地运行。这些技术包括链接海量节点的方法,创新的调度方式等。更多的技术点我会在后续博客中解释。

TurboFan 依赖于通过内联缓存收集的输入类型信息,相关功能通过Ignition 解释器运行。使用这些信息可以生成足以处理不同情况的最优代码。

编译器需要考虑的输入类型的变化越少,生成的代码就越小,越快。因此,你可以通过保持函数的单态性或最小化多态性来帮助TurboFan 提升代码运行速度。
- 单态性:一个输入类型
- 多态性:两到四种输入类型
- 变态性:五种或者更多的输入类型

用Deoptigate检查性能特性

与其盲目地追求最佳性能,我建议你首先了解下你的代码是怎样被优化编译器编译的,然后再去分析会造成性能下降的情况。

为了更加方便地实现这一点,我创建了Deoptigate项目。设计这个项目的目的就是提供方法让你去分析你的函数的单态,多态和变态性。

看一下这个简单的示例脚本,我将使用deoptigate 对其进行配置。

我定义了两个变量函数:addsubtract

function add(v1, v2) {
  return {
    x: v1.x + v2.x
  , y: v1.y + v2.y
  , z: v1.z + v2.z
  }
}

function subtract(v1, v2) {
  return {
    x: v1.x - v2.x
  , y: v1.y - v2.y
  , z: v1.z - v2.z
  }
}

接下来,我在循环体中使用相同类型(相同的属性以相同的顺序分配)的对象来执行这些函数。

const ITER = 1E3
let xsum = 0
for (let i = 0; i < ITER; i++) {
  for (let j = 0; j < ITER; j++) {
    xsum += add({ x: i, y: i, z: i }, { x: 1, y: 1, z: 1 }).x
    xsum += subtract({ x: i, y: i, z: i }, { x: 1, y: 1, z: 1 }).x
  }
}

这时addsubtract这两个函数应该会运行得比较消耗性能了,同时也会得到相应的优化。

现在我再次执行他们,将对象传递给add函数,此时已经不存在和之前相同类型的对象了,因为他们的属性是按照不同的顺序分配的({ y: i, x: i, z: i })

subtract函数传递和之前一样类型的对象。

for (let i = 0; i < ITER; i++) {
  for (let j = 0; j < ITER; j++) {
    xsum += add({ y: i, x: i, z: i }, { x: 1, y: 1, z: 1 }).x
    xsum += subtract({ x: i, y: i, z: i }, { x: 1, y: 1, z: 1 }).x
  }
}

运行代码并使用 deoptigate 来检查它。

node --trace-ic ./vector.js
deoptigate

在使用-trace-ic标记执行我们的代码是,V8把我们需要的信息写进 isolate-v8.log日志文件。当deoptigate在该文件夹下运行时,它将处理该文件并使用一个可交互的可视化方式来显示所包含的数据。

它是一个web应用程序,所以你可以在浏览器中打开它并进行后续操作。

deoptigate 为我们提供了所有文件的摘要,在我们的示例中就是vector.js文件。对于每个文件,它显示相关的优化、反优化和内联缓存信息。这里绿色表示每一问题,蓝色表示次要问题,红色表示需要查看的潜在的重要问题。我们只需要单击文件名称就可以展开文件的细节。
[每周一译]为何新的V8引擎如此的快
左侧提供了文件的源代码,并且在注释里指出了潜在的性能问题。在右侧,我们可以了解到每个问题的更多细节。两个视图的功能是串联的。单击左侧的注释就会在右侧突显出更多细节。反之亦然。
[每周一译]为何新的V8引擎如此的快
快速浏览一下我们会发现,subtract显示不存在潜在的问题,而add却有。单击代码中的红色三角形,就会在右边高亮显示出相关的反优化信息。请注意,对于使用映射(Map)错误的原因。

点击任何一个蓝色电话图标都会显示出更多的信息。我们发现函数变成了多态性。正如我们所见,这就是不正确使用映射造成的。

在页面顶部检查轻度警告信息可以获得更多有关优化的建议,这次我们还介绍了包括用于add函数的时间戳在内的优化。
[每周一译]为何新的V8引擎如此的快

我们看到add在32ms后得到了优化。在大约40ms时,它被提供了一个优化代码没有考虑的输入类型——因此造成了错误的映射——并且被去优化,此时它被降级为Ignition字节码,同时收集更多内联缓存信息,并在41ms后很快又进行了优化。

总之,add函数最终通过优化的代码执行,但是该代码需要处理两种类型的输入(不同的映射),因此代码量更加庞大,而且不像以前那样最优。

相反,subtract函数只优化了一次,所以我们可以通过点击函数签名中的绿色的三角形进行验证。
[每周一译]为何新的V8引擎如此的快

为什么是不同的映射?

有些人可能想知道为什么V8认为通过{x,y,z}赋值创建的对象与通过{y,x,z}赋值创建的对象不同。毕竟他们具有完全相同的属性,只是属性的顺序不同而已。

主要原因在于初始化JS对象时创建映射表的方式,这也是我们另一篇文章将要介绍的主题。

感谢阅读。

原文:Why the New V8 is so Damn Fast—Thorsten Lorenz|2018.07.23
参考文献:新的V8是如何重构提速的?——小大非

相关标签: 前端