发布 JetStream 2 基准测试套件

今天,我们宣布推出 JetStream JavaScript 基准测试套件的新版本,即 JetStream 2。JetStream 2 结合了各种 JavaScript 和 WebAssembly 基准测试,涵盖了一系列高级工作负载和编程技术,并报告一个通过几何平均值平衡它们的单一分数。JetStream 2 旨在奖励启动快、执行代码快、运行流畅的浏览器。优化 JavaScript 引擎的整体性能一直是 WebKit 团队的首要任务。我们使用基准测试来推动对 WebKit 引擎进行广泛且可维护的优化,这常常促使我们进行重大的架构改进,例如在 2015 年创建了全新的优化编译器 B3。JetStream 2 混合了我们多年来一直在优化的工作负载,以及未来几年我们将继续改进的新工作负载。

我们在 2014 年发布 JetStream 1 时,它是一个涵盖广泛的基准测试,衡量了当时 JavaScript 生态系统中最新功能的性能。JetStream 1 的主要目标之一是提供一个可用于衡量 JavaScript 整体性能的单一基准测试。然而,自 2014 年以来,JavaScript 生态系统发生了许多变化。两大主要变化是 ES6 中 JavaScript 语言的广泛更新发布,以及 WebAssembly 中全新语言的引入。尽管我们创建 JetStream 1 是为了跟踪整体引擎性能,但在其发布后的几年里,我们发现自己不断创建和使用新的基准测试。我们在 2017 年创建了 ARES-6,以衡量我们在 ES6 工作负载上的性能。在过去两年中,我们还在 WebKit 团队内部跟踪了两个 WebAssembly 基准测试。2017 年底,V8 团队发布了 Web Tooling Benchmark,我们使用该基准测试来改进 JavaScriptCore 中的正则表达式性能。尽管 JetStream 1 是在 Kraken 发布后才发布的,但我们发现其发布后仍然继续跟踪 Kraken,因为我们喜欢其中的某些子测试。总而言之,在创建 JetStream 2 之前,WebKit 团队一直在跟踪其在六个不同 JavaScript 和 WebAssembly 基准测试中的性能。

将 JavaScriptCore 的性能工作基于六个不同基准测试套件的相对排序来衡量是不切实际的。事实证明,根据六个不同基准测试套件的综合结果来评估一项更改的价值并非显而易见。通过 JetStream 2,我们重新回归到使用单一基准测试——它平衡其子测试的分数以生成一个单一的总体分数——来衡量整体引擎性能。JetStream 2 汲取了这六个先前基准测试的最佳部分,增加了一组新的基准测试,并将它们组合成一个全新的单一基准测试套件。JetStream 2 由以下部分组成:

  • 几乎所有 JetStream 1 基准测试。
  • 受 Kraken 子测试启发的新基准测试。
  • 所有 ARES-6 基准测试。
  • Web Tooling Benchmark 的大约一半。
  • 源自 JetStream 1 的 asm.js 基准测试的新 WebAssembly 测试。
  • 涵盖其他六个基准测试套件未测试领域的新基准测试。

新基准测试

JetStream 2 增加了衡量这些先前基准测试套件未涵盖领域的新基准测试。JetStream 1 的一个重要组成部分是 asm.js 基准测试子集。随着 WebAssembly 的发布,asm.js 的重要性有所降低,因为许多 asm.js 用户现在正在使用 WebAssembly。我们将 JetStream 1 中的许多 asm.js 基准测试转换成了 JetStream 2 中的 WebAssembly。我们还创建了一些新的 WebAssembly 基准测试。其中一个新的测试是 richards-wasm,它模拟了一个混合 JS 和 WebAssembly 应用程序。Richards-wasm 是 Martin Richard 系统语言基准测试的实现,部分用 WebAssembly 实现,部分用 JavaScript 实现。该基准测试模拟了一个频繁调用 WebAssembly 中定义辅助方法的 JavaScript 应用程序。

JetStream 2 增加了两项新的 Web Worker 测试。鉴于 Web Worker 在网络上的普及程度,JetStream 2 衡量其性能至关重要。我们创建的第一个 Worker 基准测试是 bomb-workers。Bomb-workers 并行运行 SunSpider 的所有内容——因此它强调了浏览器在并行运行大量 JavaScript 时的处理能力。我们创建的第二个基准测试是 segmentation。Segmentation 并行运行四个 Web Worker,以计算样本数据集上的时间序列分割。该基准测试源自 WebKit 性能仪表板中使用的相同算法。

JetStream 2 增加了三个强调正则表达式性能的新基准测试:OfflineAssemblerUniPokerFlightPlanner。UniPoker 和 FlightPlanner 强调 Unicode 正则表达式的性能——这是 ES6 中新增的功能。OfflineAssembler 是从 Ruby 翻译成 JavaScript 的 JavaScriptCore 离线汇编器解析器和 AST 生成器。UniPoker 是一个五张牌梭哈扑克模拟,使用 Unicode 码点表示扑克牌值。FlightPlanner 解析由命名航段组成的飞机飞行计划,例如起飞爬升巡航等,这些航段包含航点、航线或方向和时间,然后使用飞机概况处理这些航段,以计算飞行计划中每个航段的航向、距离和预测时间,以及总飞行计划。它会运行 Unicode 正则表达式代码路径,因为航段名称是俄语。

JetStream 2 还增加了两个新的通用 JavaScript 基准测试:async-fsWSL。Async-fs 模拟执行各种文件系统操作,例如添加和删除文件,以及交换现有文件的字节顺序。Async-fs 强调 DataView、Promise 和异步迭代的性能。WSL 是 WHLSL 早期版本的一个实现——这是一项针对网络的新着色语言提案。WSL 衡量整体引擎性能,特别强调各种 ES6 构造和 throw

您可以在摘要页面中阅读 JetStream 2 中每个基准测试的详细信息。

基准测试方法

JetStream 2 中的每个基准测试都衡量不同的工作负载,单一的优化技术不足以加速所有基准测试。有些基准测试会表现出权衡,对一个基准测试进行激进或专门的优化可能会使另一个基准测试变慢。JetStream 2 中的每个基准测试都会计算一个单独的分数。JetStream 2 对每个基准测试的权重相同。

在衡量网络上的 JavaScript 性能时,仅仅考虑工作负载的总运行时间是不够的。浏览器对相同的 JavaScript 工作负载的性能可能因运行次数而异。例如,垃圾回收会定期运行,导致某些迭代比其他迭代耗时更长。重复运行的代码会由浏览器进行优化,因此任何工作负载的第一次迭代通常比其余的更耗时。

因此,JetStream 1 将每个基准测试分为两类:延迟吞吐量。延迟测试衡量启动性能或最差情况性能。吞吐量测试衡量持续的峰值性能。与 JetStream 1 一样,JetStream 2 衡量启动、最差情况和峰值性能。然而,与 JetStream 1 不同的是,JetStream 2 会针对每个基准测试衡量这些指标。

JetStream 2 对 JavaScript 和 WebAssembly 的评分方式不同。在 JetStream 2 中,除一个 JavaScript 基准测试外,其他所有基准测试的单独分数都平等地权衡启动性能、最差情况性能和平均情况性能。这三个指标对于在浏览器中运行高性能 JavaScript 至关重要。快速的启动时间使得浏览器加载页面更快,并允许用户更早地与页面交互。良好的最差情况性能确保 Web 应用程序运行流畅,没有卡顿或视觉抖动。快速的平均情况性能使得最先进的 Web 应用程序能够运行。为了衡量这三个指标,每个基准测试运行 N 次迭代,N 通常是 120,但可能会根据每次迭代的总运行时间而变化。JetStream 2 将启动分数报告为运行第一次迭代所需的时间。最差情况分数是最差的 M 次迭代的平均值,不包括第一次迭代。M 总是小于 N,通常为 4。对于某些基准测试,当 N 小于 120 时,M 可以小于 4。平均情况分数是除第一次迭代外的 N 次迭代的平均值。这三个指标使用几何平均值进行同等加权。

WSL 是 JetStream 2 中唯一一个不采用上述评分技术的 JavaScript 基准测试。WSL 采用不同的评分机制,因为它运行单次迭代所需的时间比其他基准测试长好几个数量级。它转而将其分数计算为两个指标的几何平均值:编译 WSL 标准库所需的时间,以及运行 WSL 规范测试套件所需的时间。

JetStream 2 的 WebAssembly 基准测试分为两部分评分,平等地权衡启动时间和总执行时间。第一部分是启动时间,即直到 WebAssembly 模块实例化所需的时间。这是浏览器将 WebAssembly 代码置于可运行状态所需的时间。第二部分是执行时间。这是在实例化后运行基准测试工作负载所需的时间。这两个指标对于 JetStream 2 进行衡量至关重要。良好的启动性能使得 WebAssembly 应用程序加载迅速。良好的执行时间使得 WebAssembly 基准测试运行快速流畅。

JetStream 2 共包含 64 个基准测试。每个基准测试的分数按上述方式计算,最终分数是这 64 个分数的几何平均值。

优化正则表达式

Web Tooling Benchmark 让我们意识到了 JavaScriptCore 正则表达式处理中的一些性能缺陷,并为我们所做的更改提供了衡量性能改进的绝佳方式。背景介绍一下,JavaScriptCore 正则表达式引擎(也称为 YARR)同时具有 JIT(即时编译)和解释器匹配引擎。WebTooling Benchmark 测试中常用的一些模式类型在 YARR JIT 中 JavaScriptCore 尚不支持。这包括反向引用,以及嵌套的贪婪和非贪婪组。

反向引用的形式为 /^(x*) 123 \1$/,其中我们匹配括号组中的内容,然后在稍后的字符串中,当引用先前组时,再次匹配相同的内容。对于此处给出的示例,字符串 "x 123 x" 将匹配,字符串 “xxxxx 123 xxxxx” 也会匹配。它匹配任何以 0 个或更多 'x' 开头,中间有 " 123 ",然后以与字符串开头相同数量的 'x' 字符结尾的行。已为 Unicode 和非 Unicode 模式添加了对反向引用的 JIT 支持。我们还为带有忽略大小写标志的模式添加了 JIT 支持,以处理 ASCII 和 Latin1 字符串。由于需要大量的字符折叠数据,Unicode 忽略大小写模式的反向引用匹配更成问题。因此,对于包含反向引用且也忽略字符大小写的 Unicode 正则表达式,我们回退到 YARR 解释器。

在讨论我们对正则表达式引擎所做的下一个改进之前,需要一些背景知识。YARR 正则表达式引擎使用回溯算法。当我们要匹配像 /a[bc]*c/ 这样的模式时,我们会正向处理模式,当匹配一个项失败时,我们回溯到前一个项,看看是否可以尝试以不同的方式匹配。[bc]* 项将尽可能多地连续匹配 b 和/或 c。字符串 “abc” 匹配该模式,但在第一次匹配 'c' 时需要回溯。发生这种情况是因为中间的 [bc]* 项会同时匹配 'b''c',当我们尝试匹配模式中的最后一个 'c' 项时,字符串已经耗尽。我们回溯到 [bc]* 项,并将该项的匹配长度从 “bc” 缩短到仅 “b”,然后匹配最终的 'c' 项。此算法要求为各种项类型保存状态信息,以便我们能够回溯。在开始这项工作之前,回溯状态由计数和指向主体字符串中项进度的指针组成。

我们不得不扩展回溯状态的保存方式,以支持嵌套在更长模式中的带计数的捕获括号组。考虑一个像 /a(b|c)*bc/ 这样的模式。除了捕获括号组内容的后向跟踪信息外,我们还需要保存该组的匹配计数以及作为捕获组后向跟踪状态一部分的起始和结束位置。此状态保存在单向链表堆栈结构中的节点上。这些括号组回溯节点的大小是可变的,取决于所有嵌套项(包括嵌套捕获组的范围)所需的状态量。每当我们开始匹配一个括号组时,无论是第一次还是后续时间,我们都会将该组中所有嵌套项的状态作为新节点保存到此堆栈上。当我们回溯到括号组的开头,尝试更短的匹配或回溯到先前的项时,我们弹出捕获组的回溯节点并恢复保存的状态。

我们还进行了另外两项有助于 JetStream 2 性能的改进。一项是同时匹配更长的常量字符串,另一项是规范化构造的字符类。我们长期以来一直有将模式中多个相邻的固定字符作为一组进行匹配的优化,一次可以匹配多达 32 位字符数据,例如四个 8 位字符。考虑表达式 /starting/,它只是查找字符串 “starting”。这些优化使我们能够通过一个 32 位加载-比较-分支序列匹配 “star”,然后通过第二个 32 位加载-比较-分支序列匹配后面的 “ting”。最近的更改是针对 64 位平台进行的,它允许我们一次匹配八个 8 位字符。通过此更改,现在可以使用单个 64 位加载-比较-分支序列来匹配此正则表达式。

我们对正则表达式所做的最新性能改进,为 JetStream 2 带来了一些好处,那就是合并字符类。背景知识:字符类可以由单个字符、字符范围、内置转义字符或内置字符类构成。在此更改之前,字符类 [\dABCDEFabcdef](它匹配任何十六进制数字)会通过将字符值与 '0' 和 '9' 进行比较来检查数字字符,然后单独将其与每个字母字符('A''B' 等)进行比较。尽管此字符类可以写成 [\dA-Fa-f],但 YARR 为 JavaScript 开发者进行此类优化是合理的。我们对字符类合并的更改现在实现了这一点。我们将相邻的单个字符合并成范围,并将相邻的范围合并成更大的范围。这通常会减少比较和分支指令的数量。在此优化之前,/[\dABCDEFabcdef]/.exec(“X”) 需要 14 次比较和分支才能确定没有匹配项。现在只需要 4 次比较和分支。

这些优化对某些 Web Tooling Benchmark 测试的性能影响是巨大的。添加 JIT 贪婪嵌套括号的能力使 coffeescript 的性能提高了 6.5 倍。JIT 非贪婪嵌套括号使 espree 的性能提高了 5.8 倍,使 acorn 的性能提高了 3.1 倍。JIT 反向引用使 coffeescript 的性能又提高了 5 倍,在该测试中总共提高了 33 倍。这些更改对其他 JetStream 2 测试也有较小但仍可衡量的改进。

性能结果

了解 JavaScriptCore 性能状态的最佳方式是随着时间的推移跟踪我们的性能,并将其与其他 JavaScript 引擎的性能进行比较。本节比较了先前发布的 Safari 版本 Safari 12.0.3、macOS 10.14.4 中新发布的 Safari 12.1,以及最新发布的 Chrome 版本 Chrome 73.0.3683.86 和 Firefox 版本 Firefox 66.0.1 的性能。

所有数据均在一台 2018 款 MacBook Pro(13 英寸,四个 Thunderbolt 3 端口,MacBookPro15,2)上收集,配备 2.3GHz Intel Core i5 处理器和 8GB 内存。Safari 12.0.3 的数据是在 macOS 10.14.3 上收集的。Safari 12.1、Chrome 73.0.3683.86 和 Firefox 66.0.1 的数据是在 macOS 10.14.4 上收集的。这些数据是每个浏览器中 JetStream 2 基准测试运行五次的平均值。每次运行之间,每个浏览器都被退出并重新启动。

JetStream 2 Scores. Bigger is Better.

上图显示,Safari 12.1 是运行 JetStream 2 速度最快的浏览器。它比 Safari 12.0.3 快 9%,比 Chrome 73.0.3683.86 快 8%,比 Firefox 66.0.1 快 68%。

结论

JetStream 2 是 JetStream 基准测试套件的一次重大更新,我们很高兴今天与您分享它。JetStream 2 包含一组多样化的 64 个 JavaScript 和 WebAssembly 基准测试,使其成为我们发布过的最广泛和最具代表性的 JavaScript 基准测试。我们相信,针对此基准测试进行优化的引擎将带来更好的网络 JavaScript 性能。JavaScriptCore 团队致力于改进 JetStream 2,并且已经在 Safari 12.1 中实现了 9% 的提升。

我们很乐意听取您对 JetStream 2 或我们为其所做优化的任何反馈意见。如果您有任何反馈,请通过 Twitter 联系 SaamMichael