【FCon上海】与行业领袖共话AI大模型、数字化风控等前沿技术。 了解详情
写点什么

V8 有了全新的超快速非优化 JS 编译器,性能提高 5-15%

V8团队

  • 2021-05-28
  • 本文字数:4563 字

    阅读完需:约 15 分钟

V8有了全新的超快速非优化JS编译器,性能提高5-15%

AI 大模型超全落地场景&金融应用实践,8 月 16 - 19 日 FCon x AICon 大会联诀来袭、干货翻倍!

V8 引入全新的非优化 JS 编译器:Sparkplug


想要编写高性能的 JavaScript 引擎,光是有高度优化的编译器(如 TurboFan)是不够的。特别是对于短生命周期的会话(例如加载网站或命令行工具),在高优化编译器开始优化之前就已经有很多工作要做,更没有时间去生成什么优化代码了。

 

正因如此,自 2016 年起,我们不再跟踪综合基准测试(如 Octane)的成绩,而是转而去衡量实际场景中的性能表现。并且从那时起,我们就一直在努力研究如何提升高优化编译器作用范围之外的 JavaScript 性能。这意味着我们需要在解析器、流式处理、对象模型、垃圾收集中的并发性、缓存编译后的代码等事项上逐个攻关……每一个领域都有新鲜的感觉。

 

当我们转向提升现实场景中初始 JavaScript 的执行性能,我们在优化解析器时开始遇到诸多局限。V8 的解析器经过高度优化,速度极快,但解析器总有一些固有开销是我们无法摆脱的;字节码解码开销或调度开销是解析器功能的内在组成部分。

 

基于我们目前的双编译器模式,我们很难更快地升级(tier-up)到优化代码;我们可以(并且正在)提升优化的效果,但在某些时候,想要提升速度就只能去掉一些优化项,但这会降低峰值性能。更糟糕的是,我们还无法提前优化进程,因为我们还没有稳定的对象形态反馈。

 

今天我们向大家介绍 Sparkplug:这是我们将随 V8 v9.1 发布的,全新的非优化 JavaScript 编译器,位于 Ignition 解析器和 TurboFan 优化编译器之间。



新的编译器管道

这是一款速度很快的编译器

Sparkplug 的设计目标是快速编译。非常快,如此之快,让我们可以随时随地进行编译,于是我们就可以比 TurboFan 代码更积极地升级到 Sparkplug 代码。

 

Sparkplug 编译器的速度来自于一些技巧。首先,它会作弊;它所编译的函数已经被编译为字节码,并且字节码编译器已经完成了大多数艰苦的工作,例如变量解析、弄清楚括号是否实际上是箭头函数、消除结构化语句等等。Sparkplug 从字节码而不是 JavaScript 源代码进行编译,因此不必操心这些麻烦的事情。

 

第二招是,Sparkplug 不会像大多数编译器那样生成任何中间表示(IR)。相反,Sparkplug 通过字节码的一次线性 pass 直接编译为机器码,并发出与该字节码的执行相匹配的代码。实际上,整个编译器是一个 for 循环内的一个 switch 语句,分派给固定的,按字节码的机器码生成函数。

for (; !iterator.done(); iterator.Advance()) {  ​VisitSingleBytecode()}
复制代码

缺少 IR 意味着编译器的优化机会有限,只能做一些非常本地的小幅度优化。这也意味着我们必须将整个实现分别移植到我们支持的每种架构上,因为这里没有架构无关的中间阶段。但事实证明这些都不是问题:快速编译器是简单编译器,因此代码很容易移植;并且 Sparkplug 不需要大量优化,因为我们稍后会在管道中提供优化效果很出色的编译器。

 

从技术上讲,我们目前对字节码进行了两次 pass——一次用来发现循环,第二次生成实际代码。不过,我们最终的计划是摆脱第一个。

解析器兼容框架

向现有的成熟 JavaScript VM 添加新的编译器是一项艰巨的任务。除了标准执行之外,你还需要支持各种各样的事情;V8 有一个调试器、一个 stack-walking CPU profiler、针对异常的堆栈跟踪、集成到升级、堆栈替换以优化代码实现热循环……实在很多。

 

Sparkplug 巧妙地简化了所有这些问题,具体方法就是保持一个“与解析器兼容的堆栈框架”。

 

稍微解释下。堆栈框架(Stack frame)是代码执行存储函数状态的方式。每当你调用一个新函数时,它都会为该函数的局部变量创建一个新的堆栈框架。一个堆栈框架由一个框架指针(标记其开始)和一个堆栈指针(标记其结束)定义:



堆栈框架,带有堆栈和框架指针

 

看到这里,很多读者会表示抗议:“这张图不对啊,堆栈明显是朝着相反的方向的!”。别急,我为你做了一个按钮:

 

当一个函数被调用时,返回地址被推入这个堆栈;该函数返回时会弹出它,来知道该返回到何处。然后,当该函数创建一个新框架时,它将旧的框架指针保存在堆栈上,并将新的框架指针设置为指向它自己的堆栈框架的起始。因此,这个堆栈有了一个框架指针链,每个框架指针都指向前一个框架的起始:



多个调用的堆栈框架

 

严格来说这只是一个约定,后面是生成的代码,它不是必需的。不过这是一种相当常见的方式;唯一真正中断的一次是堆栈框架完全清除的时候,或者可以改用调试边表(side-table)遍历堆栈框架的时候。

 

这是针对所有函数类型的常规堆栈布局;然后是关于如何传递参数,以及函数如何在其框架中存储值的约定。在 V8 中,我们有针对 JavaScript 框架的约定,即在调用函数之前将参数(包括接收器)以相反的顺序推入堆栈,并且堆栈上的前几个槽为:被调用的当前函数;被调用的上下文;以及传递的参数数量。这是我们的“标准”JS 框架布局:



一个 V8 JavaScript 堆栈框架

 

这个 JS 调用约定在优化框架和解析框架之间共享,这样一来,当我们在调试器的性能面板中调优代码时,就能以最小的开销遍历堆栈,诸如此类。

 

对于 Ignition 解析器来说,约定变得更加显式。Ignition 是基于寄存器的解析器,这意味着存在一些虚拟寄存器(请勿与机器寄存器混淆!)来存储解析器的当前状态——其中包括 JavaScript 函数的本地变量(var/let/const 声明)和临时值。这些寄存器与要执行的字节码数组指针,以及该数组中当前字节码的偏移量一起存储在解析器的堆栈框架中:



V8 解析器堆栈框架

 

Sparkplug 会有意创建并维护一个与解析器的框架相匹配的框架布局;只要解析器​​存储一个寄存器值,Sparkplug 也会存储一个值。这样做有几个原因:

 

  1. 它简化了 Sparkplug 的编译过程;Sparkplug 可以只镜像解析器的行为,而无需保留从解析器寄存器到 Sparkplug 状态的某种映射。

  2. 由于字节码编译器完成了分配寄存器的重活儿,因此它还加快了编译速度。

  3. 它大大简化了与系统其余部分的集成工作。调试器、profiler、异常堆栈展开、堆栈跟踪打印,所有这些操作都会执行堆栈遍历以发现当前正在执行的函数堆栈,并且所有这些操作都不需要做什么更改就能继续搭配 Sparkplug,因为就它们而言,它们有的只是一个解析器框架。

  4. 它简化了堆栈替换(OSR)。OSR 是指在执行过程中替换当前正在执行的函数;当前,当一个已解析函数在一个热循环内(在该循环中它升级为优化代码),以及在优化代码取消优化(在其降级并继续在解析器中执行该函数)时,就会发生这种情况。使用 Sparkplug 框架镜像解析器框架时,任何适用于解析器的 OSR 逻辑都将适用于 Sparkplug;更棒的是,我们可以在解析器和 Sparkplug 代码之间切换,而框架转换开销几乎为零。

 

我们对解析器堆栈框架做了一个小更改,即在 Sparkplug 代码执行期间,我们不让字节码偏移保持最新。相反,我们存储一个从 Sparkplug 代码地址范围到对应的字节码偏移量的双向映射。这是一种相对简单的编码映射,因为 Sparkplug 代码是直接从字节码上的一个线性遍历发出的。每当一个堆栈框架访问想要知道一个 Sparkplug 框架的“字节码偏移量”时,我们都会在此映射中查找当前执行的指令,并返回相应的字节码偏移量。类似地,每当我们想将 OSR 从解析器转换为 Sparkplug 时,我们都可以在映射中查找当前字节码偏移量,然后跳转到相应的 Sparkplug 指令。

 

你可能会注意到,我们现在在堆栈框架上有一个未使用的插槽,字节码偏移量就会在这个插槽上。由于我们希望保持堆栈的其余部分不变,因此我们不能放弃它。我们重新调整了这个堆栈插槽的功能,让它为当前正在执行的函数缓存“反馈向量”。这是用于存储对象形态数据的向量,大多数操作都需要加载它。我们要做的只是谨慎一点对待 OSR,确保我们为这个插槽要么换入正确的字节码偏移量,要么换入正确的反馈向量。

 

于是 Sparkplug 堆栈框架为:



一个 V8 Sparkplug 堆栈框架

交给内置代码

实际上,Sparkplug 很少生成自己的代码。JavaScript 语义很复杂,即使执行最简单的操作也需要大量代码。由于多种原因,强制 Sparkplug 在每次编译时内联重新生成这些代码都是不好的:

 

  1. 由于需要生成大量代码,这将明显增加编译时间,

  2. 这会增加 Sparkplug 代码的内存消耗,并且

  3. 我们必须重新实现用于 Sparkplug 的一堆 JavaScript 功能的代码源,这可能意味着会有更多的错误和更大的受攻击面。

 

因此,大多数 Sparkplug 代码只是调用“内置代码”,即嵌入二进制文件中的小段机器码片段,以完成那些脏活儿。这些内置代码要么就是解析器用的那些,或者至少与解析器的字节码处理程序共享大部分代码。

 

实际上,Sparkplug 代码基​​本上只是内置代码的调用和控制流:

 

你现在可能会想,“那么,这一切到底有什么意义?Sparkplug 不是在做与解析器相同的工作吗?”——你的疑问是有道理的。在许多方面,Sparkplug 只是解析器执行的一个序列化,它调用相同的内置函数并维护相同的堆栈框架。但这样做也是值得的,因为它消除(或更准确地说是预编译)了那些不可移动的解析器开销,例如操作数解码和下一个字节码分派。

 

事实证明,解析器破坏了许多 CPU 优化工作:解析器从内存中动态读取静态操作数,从而迫使 CPU 停顿或推测值可能是多少。分派到下一个字节码需要成功的分支预测才能保持高性能,即使推测和预测正确,你还是要执行所有解码和分派代码,并且你还是会在各个缓冲区和缓存中浪费宝贵的空间。CPU 实际上本身就是一个解析器,只不过它是机器码的解析器。这样看来,Sparkplug 是从 Ignition 字节码到 CPU 字节码的一个“转译器”,将你的函数从在“仿真器”中运行移到了“原生”运行。

性能表现

那么,Sparkplug 在现实场景中的性能表现如何呢?我们用 Chrome M91 跑了一些基准测试,用了几个性能 bot,分别启用和关闭 Sparkplug 来观察其影响。

 

剧透:我们非常满意。

 

以下基准测试列出了运行多个操作系统的 bot。虽说系统和 bot 的名字差不多,但我们认为它并不会对结果产生太大影响。另外,不同的机器也有不同的 CPU 和内存配置,我们认为这是差异的主要来源。

Speedometer

Speedometer 是一个基准测试,它使用一些流行的框架构建一个 TODO 列表跟踪 Web 应用程序,并通过添加和删除 TODO 对应用程序进行性能压力测试,来模拟现实世界中网站框架的使用情况。我们发现它很好地反映了现实世界中的负载和互动行为,并且我们屡屡发现,Speedometer 的成绩提升反映在了我们的现实世界指标中。

 

使用 Sparkplug,Speedometer 得分提高 5-10%,具体取决于我们观察的 bot。



使用 Sparkplug 在多个性能 bot 中改善了 Speedometer 的得分中位数。误差线表示四分位间距。

浏览基准测试

Speedometer 是一个很好的基准测试,但它只反映了部分情况。此外,我们还有一组“浏览基准测试”,它们记录了一组真实的网站,我们可以重播这些内容、编写一些交互脚本,并更真实地了解我们的各种指标在现实世界中的表现。

 

在这些基准测试上,我们选择查看“V8 主线程时间”指标,其测试主线程(不包括流解析或后台优化的编译)在 V8 中花费的总时间(包括编译和执行)。这是在不排除其他基准噪声源的情况下查看 Sparkplug 自身回报的最佳方法。

 

结果各不相同,并且完全取决于机器和网站,但总体而言它们看起来不错:我们看到大约有 5-15%的改进。



在我们的浏览基准测试中,V8 主线程时间得到了 10 个百分点的中位数改进。误差线表示四分位间距。

 

结论:V8 有了全新的超快速非优化编译器,可将 V8 在实际基准测试中的性能提高 5-15%。V8 v9.1 中已经在--sparkplug 标志后面提供了这一工具,并且随着 M91 的发布,我们将在 Chrome 中推出该编译器。

 

原文链接:https://v8.dev/blog/sparkplug

2021-05-28 12:203903

评论

发布
暂无评论
发现更多内容

作业4

施正威

牛掰,在IDEA中,你可以安装小傅哥写的插件了!

小傅哥

小傅哥 idea插件 vo2dto转换插件开发 idea plugin 代码织入

智慧巨鹿使用Rainbond落地实践,一个平台管理所有应用系统

北京好雨科技有限公司

「自我检验」熬夜总结50个Vue知识点,全都会你就是神!!!

Sunshine_Lin

面试 Vue 前端 进阶 ES6

作业5

施正威

SpringMVC框架基础知识(03)

海拥(haiyong.site)

28天写作 1月月更

功能十分全面的项目管理工具(文档协作、项目计划、文件管理等)

优秀

项目管理 项目管理工具

【LeetCode】Bigram 分词 Java题解

Albert

算法 LeetCode 1月月更

百分点数据科学实验室:零售行业新店品类配比测算方案

百分点科技技术团队

架构实战营第 4 期 -- 模块五作业

烈火干柴烛灭田边残月

架构实战营

MongoDB常用命令(2)

Tom弹架构

Java mongodb 分布式数据库

LevelDB在测试中应用应用

FunTester

数据库 测试开发 leveldb FunTester 有状态服务

Git操作备忘录(持续更新)

IT蜗壳-Tango

git IT蜗壳教学 1月月更

nodejs 异步I/O和事件驱动

编程江湖

nodejs

一文了解区块链如何帮助打击虚假信息

CECBC

spring源码搭建

派大星

Spring5源码解析

荣获中国专利金奖!百度连续四年AI专利申请和授予量全国第一

百度开发者中心

人工智能 AI

作业6

施正威

一文整理区块链技术为企业带来的九大好处

CECBC

SphereEx 完成近千万美元 Pre-A 轮融资,加速构建新一代数据库生态引擎

SphereEx

开源 融资 ShardingSphere SphereEx 嘉御资本

数字化转型失败,有哪些原因?

禅道项目管理

数字化转型

What's new in dubbo-go-pixiu 0.4.0

apache/dubbo-go

Cube 技术解读 | Cube 小程序技术详解

蚂蚁集团移动开发平台 mPaaS

小程序 前端 mPaaS cube

网络安全好学吗?基础入门篇,NMAP高级使用技巧和漏洞扫描发现

学神来啦

网络安全 渗透测试 kali基础 nmap kali Linux

TDengine在上海电气储能智慧运维系统中的应用

TDengine

数据库 大数据 tdengine

恒源云(GPUSHARE)_云GPU服务器如何使用LightGBM?

恒源云

人工智能 算力 框架学习

终极解密输入网址按回车到底发生了什么

码哥字节

HTTP TCP/IP

带你认识7种云化测试武器

华为云开发者联盟

测试 接口测试 华为云DevCloud 云化测试 Mock 服务

科技驱动经济发展的时代全面到来

CECBC

《JavaScript基础入门》环境搭建

IT蜗壳-Tango

IT蜗壳教学 1月月更

敏感信息打码就安全了吗?

恒生LIGHT云社区

信息安全

V8有了全新的超快速非优化JS编译器,性能提高5-15%_语言 & 开发_InfoQ精选文章