asm.js 速度提升无处不在

asm.js 是一个易于优化的 JavaScript 子集。它可以在所有浏览器中运行,无需插件,并且是移植 C/C++ 代码库(例如游戏引擎)的理想目标——事实上,游戏引擎一直是这种方法的最大采用者,例如 Unity 3D虚幻引擎

显然,使用 asm.js 移植游戏的开发人员希望游戏在所有浏览器中都能良好运行。但是,每个浏览器都有不同的性能特征,因为每个浏览器都拥有不同的 JavaScript 引擎、不同的图形实现等等。在这篇文章中,我们将重点关注 JavaScript 执行速度,并了解 asm.js 执行速度在各个方面所取得的显著进展。现在让我们逐一介绍四大主流浏览器。

Chrome

早在 2013 年,Google 就发布了 Octane 2.0,这是其主要 JavaScript 基准测试套件的新版本,其中包含一个新的 asm.js 基准测试,zlib。基准测试定义了浏览器优化的方向:重要的内容会被纳入基准测试,然后浏览器会竞争以取得最佳分数。因此,将 asm.js 基准测试添加到 Octane 清楚地表明了 Google 相信 asm.js 内容对于优化至关重要。

最近,另一个重大进展是 Google 发布了TurboFan,这是一个用于 Chrome JavaScript 引擎v8 的新的正在开发中的优化编译器。TurboFan 具有“节点海洋”架构(这在 JavaScript 领域是一个新概念,但在其他领域,例如 Java 服务器虚拟机中已非常成功地应用),旨在比 v8 的第一个优化编译器 CrankShaft 达到更高的速度。

虽然 TurboFan 尚未准备好用于所有 JavaScript 内容,但从 Chrome 41 开始,它已启用 asm.js。尽早为 asm.js 提供 TurboFan 的优势表明了 Chrome 团队优化 asm.js 的重要性。这种优势相当显著:例如,TurboFan 将Emscripten 的 zlib 基准测试速度提高了13%,将 fasta 速度提高了24%

Safari

在过去的一年里,Safari 的 JavaScript 引擎JavaScriptCore 引入了一种名为FTL 的新型 JIT(即时编译器)。FTL 代表“第四层 LLVM”,因为它在之前的三个优化层之上增加了第四层优化,并且它是基于LLVM 的,LLVM 是一种强大的开源编译器框架。这令人兴奋,因为 LLVM 是一种顶级通用编译器,经过多年的优化,Safari 可以利用所有这些努力。正如之前链接的博客文章中所示,FTL 提供的速度提升非常显著。

Apple 今年的另一个有趣发展是引入了一个新的 JavaScript 基准测试,JetStream。JetStream 包含多个 asm.js 基准测试,这表明 Apple 相信 asm.js 内容对于优化至关重要,就像 Google 将 asm.js 基准测试添加到 Octane 一样。

Internet Explorer

Internet Explorer 中的 JavaScript 引擎名为Chakra。去年,Chakra 团队发布博客文章介绍了即将在 Windows 10 中推出的优化套件,并指出 asm.js 工作负载在 Octane 和 JetStream 上取得了显著的性能提升。这再次证明了将 asm.js 工作负载纳入通用基准测试如何推动测量和优化。

然而,最大的消息是 Chakra 团队最近宣布他们正在努力添加特定的 asm.js 优化,这些优化将在 Windows 10 中与之前提到的其他优化一起推出。这些优化尚未出现在预览版中,因此我们无法在这里进行测量和报告。但是,我们可以根据 asm.js 优化在 Firefox 中首次推出产生的影响进行推测。正如这张包含首次推出后测量的基准测试对比幻灯片中所示,asm.js 优化使 Firefox 的速度立即提高到大约是原生性能的 2 倍(之前是原生性能的 5-12 倍)。为什么这些优势会迁移到 Chakra?因为,正如我们之前发布的文章中所解释的那样,asm.js 规范提供了一种可预测的方式来验证 asm.js 代码并根据结果生成高质量的代码。

因此,让我们期待在 Windows 10 中获得良好的 asm.js 性能!

Firefox

正如我们之前提到的,asm.js 优化在 Firefox 中的首次推出通常使 Firefox 的原始吞吐量达到原生性能的 2 倍以内。到 2013 年底,我们可以报告称差距已缩小到大约是原生性能的 1.5 倍——这与不同原生编译器之间的差异非常接近,因此与“原生速度”的比较意义不大。

从高层次上看,这种进步来自两种类型的改进:编译器后端优化和新的 JavaScript 功能。在编译器后端优化领域,有一系列微小的改进(针对特定代码模式或硬件),很难指明任何一项。不过,有两项重大改进非常突出:

除了后端优化工作之外,还有两个新的 JavaScript 功能已合并到 asm.js 中,这为硬件解锁了新的性能能力。第一个功能,Math.fround,可能看起来很简单,但它使编译器后端能够在 JS 中谨慎使用时生成单精度浮点运算。正如这篇文章中所述,这种切换可以使速度提高 5% – 60%,具体取决于工作负载。第二个功能更大:SIMD.js。这仍然是ES7 的第一阶段提案,因此新的 SIMD 操作以及相关的asm.js 扩展只在Firefox Nightly 中可用。初步结果很有希望。

除了所有这些吞吐量优化之外,Firefox 还进行了一系列加载时间优化:在主线程之外并行编译 asm.js 代码,以及缓存已编译的机器代码。正如这篇文章中所述,这些优化显著改善了启动 Unity 或 Epic 规模的 asm.js 应用程序的体验。上面提到的基准测试中现有的 asm.js 工作负载不会测试 asm.js 性能的这一方面,因此我们组建了一个名为Massive 的新基准测试套件,该套件专门测试这一方面。从Firefox 的 Massive 得分随时间的变化来看,我们可以看到加载时间优化使性能提高了 6 倍以上(有关详细信息,请参阅 Hacks 博客文章,介绍Massive 基准测试)。

结论

最终,最重要的不是底层的实现细节,甚至不是特定基准测试上的特定性能数字。真正重要的是应用程序的运行状况。检查这一点的最佳方法是实际运行真实世界的游戏!Dead Trigger 2 是一个使用 asm.js 的游戏的很好的例子,它是一款 Unity 3D 游戏。

视频显示了该游戏在 Firefox 上运行,但由于它只使用标准 Web API,因此它应该可以在任何浏览器中运行。我们现在试了一下,它在 Firefox、Chrome 和 Safari 上运行得很流畅。我们也期待在 Internet Explorer 的下一个预览版中对其进行测试。

另一个例子是Cloud Raiders

与 Unity 一样,Cloud Raiders 的开发人员能够将他们现有的 C++ 代码库(使用Emscripten)编译为在 Web 上运行,而无需依赖插件。最终的结果可以在四大主流浏览器中都很好地运行。

总之,asm.js 性能在过去一年取得了长足进步。 当然还有提升的空间 - 有时性能并不完美,或者某个特定 API 在某个浏览器中缺失 - 但所有主要浏览器都在努力确保 asm.js 能够快速运行。我们可以通过查看它们优化的基准测试(包含 asm.js)以及它们在 JavaScript 引擎中实施的新改进(通常受 asm.js 推动)来观察这一点。因此,不久前需要插件才能运行的游戏,现在很快就能在 Web 上的现代浏览器中流畅地运行,无需插件。

关于 Alon Zakai

Alon 是 Mozilla 研究团队的成员,主要从事 Emscripten 的开发工作,Emscripten 是一个将 C 和 C++ 编译成 JavaScript 的编译器。Alon 于 2010 年创立了 Emscripten 项目。

更多 Alon Zakai 的文章…

关于 Luke Wagner

Luke Wagner 是 Mozilla 的软件工程师,在 Firefox 中开发 JavaScript 和 WebAssembly。

更多 Luke Wagner 的文章…


10 条评论

  1. David Flanagan

    我真的很想有一个简单的方法来在单个独立函数中使用 asm.js,这样我就可以从常规 JavaScript 中调用它。例如,我希望在 worker 中进行图像处理时能够使用 asm.js 的速度提升。

    我还没有找到用手工编写的 asm.js 或 emscripten 实现的方法。这可能吗?如果是这样,我非常希望看到一个示例(或文档)来说明如何实现它。

    2015 年 3 月 3 日 下午 11:56

    1. Alon Zakai

      emscripten 文档中有一节介绍了这个问题,

      http://kripken.github.io/emscripten-site/docs/porting/connecting_cpp_and_javascript/Interacting-with-code.html

      有关“原始”asm.js 示例,请参阅 asm.js 规范的 GitHub 仓库

      https://github.com/dherman/asm.js/

      不过需要注意的是,如果您在 asm.js 中调用非常少量的操作,那么它可能不值得。当您调用 asm.js 来进行大量计算时,其优势将最为明显。

      2015 年 3 月 3 日 下午 12:04

    2. William Furr

      我使用 Alon 链接中的 embind 系统取得了不错的效果。如果您有外部 C 函数,Module.cwrap 会更加简单。

      如果您正在编译库代码以从 JS 中调用,那么您需要设置 -s NO_EXIT_RUNTIME=1,这将使 emscripten 运行时在主函数(如果有)完成之后仍然保持运行。

      2015 年 3 月 3 日 下午 2:48

    3. Gerard Braad

      虽然不建议,但实际上可以使用 asm.is 手动编写代码。Sebastian 就这样为 jor1k 编写了代码。

      2015 年 3 月 3 日 下午 5:36

  2. David Flanagan

    感谢您提供的链接,Alon。

    我在 Dave 的仓库中没有找到手动编写的 asm.js 模块示例,但规范中的示例(接近 http://asmjs.org/spec/latest/#introduction 的结尾)非常有用。

    以该示例为基础,我能够编写一个简单的 asm.js 模块,对数百万个像素进行模拟图像处理。令我惊讶的是,它在链接时要求堆大小必须是 2 的幂,这通常意味着我无法使用任意 ImageData.data.buffer 作为我的堆,而必须将要处理的像素复制到单独分配的堆数组中。

    不过,一旦我解决了这个问题,我就第一次成功编写了一个 asm.js 模块。不幸的是,代码在使用 asm.js 时实际上更慢了。如果我删除了“use asm”指令,或者使用错误的堆大小导致链接失败,它会更快。我怀疑这可能是由于一些开销,需要在多次调用 asm.js 函数后才能得到摊销。

    在进行这些实验的过程中,我还发现提高图像处理速度的一个好方法是从 Uint8ClampedArray 转换为 Uint8Array。仅此一项更改可能比 asm.js 对我的代码的影响更大。

    我还要补充一点,我还没有尝试使用 emscripten 编译 C 函数来进行图像处理,因为文档中一直在谈论“emscripten 运行时”,这听起来很不祥,而且似乎很大,而我想要的只是一些非常快的 JavaScript 代码,只有 20 到 50 行。

    2015 年 3 月 4 日 下午 11:37

    1. Luke Wagner

      通常情况下,堆大小不必是 2 的幂;它也可以是 16mb 的任何倍数 (http://asmjs.org/spec/latest/#linking-0)。

      速度下降似乎与在进入和退出 asm.js 的过程中花费大量时间在跳板程序中有关;目前还没有针对此调用路径的 IC。您可以在 FF 内置分析器中看到这种开销,方法是在开发者工具首选项中启用“显示 Gecko 平台数据”,然后查找 asm.js 进入/退出跳板程序中的自耗时。

      2015 年 3 月 5 日 上午 2:33

  3. Eric Morgen

    一个由 SpiderMonkey 提供支持的 node.js 分支 JXcore 使得在 node 应用程序中使用 ASM.JS 成为可能。“asm.js 在各处加速”也涵盖了这一点。

    2015 年 3 月 5 日 上午 9:42

  4. Owen Densmore

    Moz 有一个 LLVM > Emscripten > asm.js 工作流程,用于 C/C++ 和其他编译语言,这很好。希望 PNaCl 现在已经死了。

    但是为什么 JS 会成为私生子?难道不能有一个 JS > asm.js 的方法吗?或者类似 TypeScript 的转译器?或者 SweetJS 宏?

    看起来 Moz 在迎合非 JS 用户,而将 JS 开发者抛在了后面。

    2015 年 3 月 6 日 上午 9:49

    1. Alon Zakai

      首先,您无法真正将 JS 编译成 asm.js - JS 没有显式类型,而 asm.js 只有在具有显式类型的情况下才能轻松优化。

      其次,Mozilla 和其他浏览器供应商正在投入巨大的人力物力来改进和优化通用 JS。这篇文章恰好是关于 asm.js,但与投入到 asm.js 的工作量相比,它仍然非常小。

      例如,通用 JS 在使用类型化对象和 SIMD.js 后将变得更快,这两个功能正在开发和优化中。您可以在 http://arewefastyet.com/(“非装箱对象”)中看到有关类型化对象的一些工作。

      2015 年 3 月 6 日 下午 6:06

  5. Paul Topping

    您无法将像完整的 JavaScript 这样的动态编程语言编译成 asm.js,正是因为 asm.js 省略了对所有动态功能的支持。这正是它能够实现速度提升的原因。C++ 比 JavaScript 更快,无论是通过 asm.js 还是使用原生代码,因为它在运行时对数据类型了解得更多,而且也省略了所有动态功能。

    2015 年 3 月 11 日 下午 4:16

本文评论已关闭。