这篇文章由 Mozilla JavaScript 团队的 David Mandelin 撰写。
Firefox 3.5 引入了 TraceMonkey,我们新的 JavaScript 引擎,它可以跟踪循环并将它们 JIT 编译为原生 (x86/ARM) 代码。与 Firefox 3 相比,许多 JavaScript 程序在 TraceMonkey 中运行速度提高了 3-4 倍。(有关技术细节,请参阅我们的 上一篇文章。)
对于 Firefox 3.6 中的 JavaScript 性能,我们重点关注我们认为最需要改进的领域。
- 某些 JavaScript 代码在 Firefox 3.5 中未进行跟踪编译。默认情况下,Firefox UI JavaScript 和附加组件 JavaScript 禁用了跟踪,因此这些程序无法从跟踪中获益。此外,许多高级 JavaScript 功能也未进行跟踪编译。对于 Firefox 3.6,我们希望跟踪更多程序和更多 JS 功能。
- 使用 JavaScript 编写的动画通常会由于垃圾回收暂停而变得卡顿。我们希望提高 GC 性能,以缩短暂停时间并使动画更流畅。
在本文中,我将解释 Firefox 3.6 附带的最重要的 JS 性能改进。我将重点介绍哪些类型的 JS 代码运行速度更快,包括展示 Fx3.6 对 Fx3.5 进行的改进的示例程序。
浏览器 UI JavaScript 的 JIT
Firefox 在两种上下文中运行 JavaScript 代码:内容和chrome(与 Google Chrome 无关)。网页内容的一部分的 JavaScript 在内容上下文中运行。浏览器 UI 或浏览器附加组件的一部分的 JavaScript 在chrome上下文中运行,并且具有额外的权限。例如,chrome JS 可以更改主浏览器 UI,但内容 JS 不允许这样做。
可以使用 about:config
分别为内容和 chrome JS 启用或禁用 TraceMonkey JIT。由于影响 chrome JS 的错误对安全性和可靠性构成了更大的风险,因此在 Firefox 3.5 中,我们选择默认情况下禁用 chrome JS 的 JIT。经过广泛的测试,我们决定默认情况下启用 chrome JS 的 JIT,这是我们在 Fx3.5 中没有时间充分调查的事情。为 chrome 启用 JIT 应该会使 Firefox UI 和附加组件背后的 JS 运行速度更快。对于一般的浏览器使用,这种差异可能不太明显,因为 UI 是为了在旧的 JS 引擎中表现良好而设计和编码的。对于执行大量 JS 计算的附加组件,这种差异应该会更加明显。
选项 | Fx3.5 默认值 | Fx3.6 默认值 |
---|---|---|
javascript.options.jit.chrome | false | true |
javascript.options.jit.content | true | true |
垃圾收集器性能
JavaScript 是一种垃圾回收语言,因此 JavaScript 引擎必须定期回收未使用的内存。我们的垃圾收集器 (GC) 在其工作时会暂停所有 JavaScript 程序。只要暂停时间“短”,这都没问题。但是,如果暂停时间过长,即使只是一点点,也会导致动画变得卡顿。动画需要以每秒 30-60 帧的速度运行才能看起来流畅,这意味着渲染一帧的时间不应超过 17-33 毫秒。因此,超过 40 毫秒的 GC 暂停会导致卡顿,而低于 10 毫秒的暂停几乎不会被察觉。在 Firefox 3.5 中,暂停时间明显过长,并且 JavaScript 动画在网络上越来越常见,因此减少暂停时间是 Firefox 3.6 中 JavaScript 的主要目标。
演示:GC 暂停和动画
演示。
此处显示的旋转刻度盘动画说明了暂停时间。除了为刻度盘设置动画外,此演示还每秒创建一个百万个 100 个字符的字符串,因此它需要频繁进行 GC。帧延迟计量器以毫秒为单位提供帧之间的时间平均值。估计的 GC 延迟计量器提供估计的 GC 延迟的平均值,基于以下假设:如果一帧的延迟是平均延迟的 1.7 倍或更多,则该帧期间正好运行了一个 GC。(此过程可能对其他浏览器无效,因此不适用于比较不同的浏览器。还要注意,GC 时间也取决于其他活动的 JavaScript 会话,因此要直接比较两个浏览器,请在每个浏览器中打开相同的选项卡。)在我的机器上,我在 Fx3.5 中获得的估计 GC 延迟约为 80 毫秒,但在 Fx3.6 中仅为 30 毫秒。
但是,通过在 Fx3.5 中打开演示,观看一段时间,然后在 Fx3.6 中尝试它,可以更容易地看到差异。
在 Fx3.5 中,我看到频繁的暂停,并且动画看起来明显卡顿。在 Fx3.6 中,它看起来非常流畅,我甚至很难准确地判断 GC 何时运行。
Fx3.6 如何做得更好。我们对垃圾收集器和内存分配器进行了许多改进。我想提供一些关于真正缩短暂停时间的两个主要更改的技术细节。
首先,我们注意到很大一部分暂停时间花在了调用 free
以回收未使用的内存上。我们无法做太多事情来加快释放内存的速度,但我们意识到可以在单独的线程上执行此操作。在 Fx3.6 中,主 JS 线程只需将未使用的内存块添加到队列中,另一个线程在空闲时间或在单独的处理器上释放它们。这意味着具有 2 个或更多内核的机器将更多地受益于此更改。但即使只有一个内核,释放也可能会延迟到空闲时间,此时它不会影响脚本。
其次,我们知道在 Fx3.5 中运行 GC 会清除 JIT 编译的所有原生代码以及一些其他加速 JS 的缓存。原因是跟踪 JIT 和 GC 彼此之间并不了解,因此如果 GC 运行,它可能会回收已编译跟踪正在使用的对象。结果是,在 GC 之后,JS 运行速度会稍微慢一些,因为缓存和已编译的跟踪会被重新构建。这将被体验为扩展的 GC 暂停或 GC 暂停后动画的短暂卡顿。在 Fx3.6 中,我们教会了 GC 和 JIT 协同工作,现在 GC 不会清除缓存或清除原生代码,因此它在 GC 后立即恢复正常运行。
跟踪更多 JavaScript 结构
在我的 关于 Fx3.5 版本的 TraceMonkey 的文章 中,我注意到某些代码结构(例如 arguments
对象)没有被跟踪,并且没有从 JIT 获得性能改进。Fx3.6 中 JS 的主要目标是跟踪更多内容,以便更多程序可以运行得更快。我们现在确实跟踪了更多内容,特别是
- DOM 属性。DOM 对象是特殊的,并且对于跟踪编译器而言更难以处理。对于 Fx3.5,我们实现了对 DOM 方法的跟踪,但没有跟踪 DOM 属性。现在我们也跟踪 DOM 属性(以及其他“原生”C++ getter 和 setter)。我们仍然不跟踪脚本化的 getter 和 setter。
- 闭包。Fx3.5 只跟踪了涉及闭包的一些操作(我的意思是引用在词法上封闭函数中定义的变量的函数)。Fx3.6 可以跟踪使用闭包的更多程序。目前尚未跟踪的主要操作是创建修改闭包变量的匿名函数。但是,调用此类函数以及实际写入闭包变量会被跟踪。
arguments
。我们现在跟踪arguments
关键字的大多数常见用法。“奇特”用法(例如设置arguments
的元素)不会被跟踪。switch
。我们改进了跟踪使用密集打包的数字 case 标签的switch
语句时的性能。这些对于模拟器和 VM 尤其重要。
这些改进对于 jQuery 和 Dromaeo 尤其重要,它们大量使用了 arguments
、闭包和 DOM。我怀疑许多其他复杂的 JavaScript 应用程序也将从中受益。例如,我们最近从作者那里听说 此 R 树库 在 Fx3.6 中的性能要好得多。
以下是一对我们跟踪的新事物的演示。第一个在循环中设置 DOM 属性。第二个调用使用 arguments
实现的 sum 函数,我在 Fx3.6 与 Fx3.5 中都获得了大约 2 倍的速度提升。
演示:Fx3.6 跟踪 DOM 属性和 arguments
DOM 属性设置:
使用 arguments
求和:
字符串和正则表达式改进
Fx3.6 包括对字符串和正则表达式性能的若干改进。例如,regexp JIT 编译器现在支持更大类别的正则表达式,包括广受欢迎的 w+
。我们还加快了一些基本操作的速度,例如 indexOf
、match
和 search
。最后,我们使在函数内部连接多个字符串序列(构建 HTML 或其他类型的文本输出的常见操作)的速度快得多。
关于我们如何使字符串连接速度更快的技术说明:连接两个字符串 S1 和 S2 的 C++ 函数执行以下操作:分配一个足够大的缓冲区以容纳结果,然后将 S1 和 S2 的字符复制到缓冲区中。要连接两个以上的字符串,如 JS s + "foo" + t
,Fx3.5 只需从左到右一次连接两个字符串。
使用 Fx3.5 算法,要连接每个长度为 K 的 N 个字符串,我们需要执行 N-1 次内存分配,并且除了一个之外的所有分配都是针对临时字符串的。更糟糕的是,前两个输入字符串被复制了 N-1 次,下一个被复制了 N-2 次,依此类推。复制的字符总数为 K(N-1)(N+2)/2,即 O(N^2)。
显然,我们可以做得更好。我们可以执行的最小工作是将每个输入字符串恰好复制一次到输出字符串,总共复制 KN 个字符。Fx3.6 通过检测 JS 程序中的连接序列并将整个序列组合成一个使用最佳算法的操作来实现此目的。
以下是一些您可以在 Fx3.6 中尝试的运行速度更快的字符串基准测试
演示:Fx3.6 字符串操作
/w+/:
indexOf('foo'):
match('foo'):
构建 HTML:
最终想法和后续步骤
我们还进行了一些不属于上述大类的小改进。最重要的是,Adobe、Mozilla、Intel、Sun 和其他贡献者继续改进 nanojit,即 TraceMonkey 使用的编译器后端。我们改进了它对内存的使用,使跟踪记录和编译速度更快,还提高了生成的原生代码的速度。更好的 nanojit 会为在 JIT 中运行的所有 JS 提供提升。
有两件大事没有进入 Fx3.6,但将在 Firefox 的下一个版本中出现,并且已在 nightly 构建中提供
- JIT 递归。递归代码(如显式循环代码)可能是热点代码,因此应进行 JIT 处理。Nightly 构建直接 JIT 递归函数。相互递归 (g 调用 f 调用 g) 尚未被跟踪。
- AMD x64 nanojit 后端。Nanojit 现在有一个生成 AMD x64 代码的后端,这使得该平台有可能获得更好的性能。
如果您尝试 nightly 构建,您会发现许多这些演示的运行速度甚至比 Fx3.6 中的还要快!
85 条评论