不是你想象的那么快:WebAssembly 与原生代码性能对比分析

在SPEC CPU基准测试套件中,我们发现显著性能差距:编译为WebAssembly的应用程序运行速度平均慢45%(Firefox)至55%(Chrome),峰值降速达2.08倍(Firefox)和2.5倍(Chrome)。我们识别出导致性能下降的原因,部分源于优化缺失和代码生成问题,另一些则源于WebAssembly平台本身的固有特性

 💬 42 条评论 |  WebAssembly/性能 | 

阿比纳夫·詹达、鲍比·鲍尔斯、埃默里·D·伯杰、阿琼·古哈
马萨诸塞大学阿默斯特分校

摘要

当前主流网页浏览器均已支持WebAssembly——这种低级字节码旨在成为C/C++等语言代码的编译目标。WebAssembly的核心目标是实现与原生代码的性能持平;既有研究表明其已接近目标,多数编译为WebAssembly的应用程序平均运行速度仅比原生代码慢10%。然而,该评估仅限于一套科学内核测试集,每个内核约含100行代码。运行更复杂的应用程序并不现实,因为将代码编译为WebAssembly仅是难题的一部分:标准Unix API在网页浏览器环境中不可用。为解决此难题,我们构建了Browsix-Wasm——这是对Browsix [29] 的重大扩展,首次实现了未经修改的WebAssembly编译Unix应用程序直接在浏览器内运行的可能性。我们随后利用Browsix-Wasm开展了WebAssembly与原生应用性能的首个大规模对比评估。在SPEC CPU基准测试套件中,我们发现显著性能差距:编译为WebAssembly的应用程序运行速度平均慢45%(Firefox)至55%(Chrome),峰值降速达2.08倍(Firefox)和2.5倍(Chrome)。我们识别出导致性能下降的原因,部分源于优化缺失和代码生成问题,另一些则源于WebAssembly平台本身的固有特性。

元素周期表

1 引言

网页浏览器已成为运行用户端应用的最主流平台,而直到最近,JavaScript仍是所有主流浏览器唯一支持的编程语言。从编程语言设计角度看,JavaScript不仅存在诸多怪癖与陷阱,其编译效率之低也广为人知 [12, 17, 31, 30]。使用JavaScript编写或编译的应用程序通常运行速度远低于原生应用。为解决此问题,多家浏览器厂商联合开发了 WebAssembly

WebAssembly是一种低级别的静态类型语言,无需垃圾回收,并支持与JavaScript的互操作性。其目标是成为可在浏览器中运行的通用编译目标[161518]。为此,WebAssembly在设计上兼具快速编译与运行特性,支持跨浏览器及架构的移植性,并提供类型安全与内存安全的形式化保证。此前在浏览器中实现原生速度运行的尝试[13, 14, 438 ],这些尝试在相关工作中已有讨论,但均未能满足上述所有标准。

WebAssembly现已获得所有主流浏览器支持[348],并已被多种编程语言迅速采用。目前已存在针对C、 C++、C#、Go和Rust语言的后端实现[392421] 经过筛选的列表目前包含十余种其他语言 [10]。如今,使用这些语言编写的代码在编译为 WebAssembly 后,即可在任何现代设备的浏览器沙箱中安全执行。

WebAssembly的主要目标之一是比JavaScript运行得更快。例如,在介绍 WebAssembly 的论文 [18] 中,当将 C 程序编译为 WebAssembly 而不是 JavaScript(asm.js)时,其在 Google Chrome 中的运行速度提高了 34%。该论文还表明WebAssembly的性能可与原生代码媲美:在评估的24个基准测试中,有7个使用WebAssembly的运行时间与原生代码相差在10%以内,且几乎所有测试都比原生代码慢不到2倍。图1显示,WebAssembly实现方案在这些基准测试中的表现持续提升。2017年仅有7项基准测试达到原生代码1.1倍的性能,而到2019年这一数字已增至13项。

这些结果看似令人鼓舞,但引发关键质疑: 这24项基准测试是否真正代表WebAssembly的预期应用场景

WebAssembly基准测试的挑战

前述24项基准测试组成为PolybenchC基准测试套件[5],旨在衡量编译器中多面体循环优化的效果。该套件中的所有基准测试均为小型科学计算内核(如矩阵乘法和LU分解),而非完整应用程序,每个内核约100行代码。虽然WebAssembly旨在加速Web环境中的科学计算内核,但其设计目标还明确涵盖更丰富的完整应用程序场景。

WebAssembly文档重点列举了若干预期应用场景[7],包括科学计算内核、图像编辑、视频编辑、图像识别、科学可视化、仿真、编程语言解释器、虚拟机及POSIX应用程序。因此,WebAssembly 在 PolybenchC 科学内核测试中的强劲表现,并不意味着它在其他类型应用程序中同样表现优异。

我们认为,对WebAssembly更全面的评估应基于成熟的大型程序基准测试套件,例如SPEC CPU基准测试套件。事实上,SPEC CPU 2006和2017基准测试套件包含多个符合WebAssembly预期用例的应用程序:其中八个基准测试属于科学应用(如433.milc、444.namd、 447.dealII、450.soplex和470.lbm),两项涉及图像视频处理(464.h264ref和453.povray),且所有基准测试均为POSIX应用程序。

遗憾的是,复杂的原生程序无法直接编译为WebAssembly。包括SPEC CPU套件在内的原生程序需要操作系统服务支持,例如文件系统、同步I/O和进程管理,而WebAssembly和浏览器均不提供这些功能。SPEC基准测试框架本身就需要文件系统、shell环境、进程创建能力及其他Unix设施。为克服将原生应用移植到网络时的这些限制,许多程序员费尽心血修改程序以规避或模拟缺失的操作系统服务。修改SPEC CPU等知名基准测试不仅耗时,还会严重威胁测试结果的有效性。
图0:不是你想象的那么快:WebAssembly 与原生代码性能对比分析

当前运行这些应用程序的标准方案是使用Emscripten——一套将C/C++编译为WebAssembly的工具链[39]。遗憾的是,Emscripten仅支持最基础的系统调用,无法满足大型应用程序的需求。例如,为使应用程序支持同步I/O,默认的Emscripten MEMFS文件系统会在程序执行前将整个文件系统映像加载至内存。对于SPEC测试而言,这些文件过大而无法完全容纳于内存中。

一种颇具前景的替代方案是使用Browsix框架,该框架支持在浏览器中运行未经修改且功能完整的Unix应用程序 [2829]。Browsix通过JavaScript实现了兼容Unix的内核,完整支持进程、文件、管道、阻塞式I/O等Unix特性。此外它还内置基于Emscripten的C/C++编译器,使程序无需修改即可在浏览器中运行。Browsix的案例研究涵盖复杂应用程序,例如完全在浏览器中运行的LaTeX,且无需任何源代码修改。

遗憾的是,Browsix仅支持JavaScript环境——因其开发于WebAssembly发布之前。此外该方案存在显著性能开销,这将成为基准测试中的重大干扰因素。使用Browsix时,难以区分基准测试中的低效表现与Browsix本身引入的性能损耗。

贡献

  • •Browsix-Wasm:我们开发了Browsix-Wasm,这是对Browsix的重要扩展与增强,可将Unix程序编译为WebAssembly并在浏览器中无修改运行。除功能扩展外,Browsix-Wasm还融入性能优化方案,大幅提升运行效率,确保CPU密集型应用几乎不受Browsix-Wasm开销影响(§​2
  • •Browsix-SPEC: 我们开发了Browsix-SPEC测试框架,该框架扩展了Browsix-Wasm功能,可自动采集详细的时序数据及硬件片上性能计数器信息,从而实现应用性能的精细化测量(§​3
  • •WebAssembly性能分析:我们运用Browsix-Wasm与Browsix-SPEC,首次基于SPEC CPU基准测试套件(2006版与2017版)对WebAssembly进行全面性能评估。该评估证实WebAssembly确实比JavaScript运行更快(在SPEC CPU测试中平均快1.3倍)。然而与先前研究相反,我们发现WebAssembly与原生性能存在显著差距:编译为WebAssembly的代码在Chrome中平均慢1.55倍,在Firefox中慢1.45倍(§​4
  • •根源分析与实施建议:我们借助性能计数器结果进行深入分析,识别出性能差距的根本原因,发现以下结果:
    1. 1.WebAssembly生成的指令包含比原生代码更多的加载和存储操作(Chrome中加载量高出2.02倍,存储量高出2.30倍;Firefox中加载量高出1.92倍,存储量高出2.16倍)。我们认为这源于寄存器可用性降低、寄存器分配器效率不足,以及未能有效利用更广泛的x86寻址模式。
    2. 2.WebAssembly生成的指令包含更多分支,因其需要执行若干动态安全检查。
    3. 3.由于WebAssembly生成更多指令,导致L1指令缓存缺失次数增加。

    我们提供优化指南,帮助WebAssembly实现者聚焦优化方向,以缩小WebAssembly与原生代码的性能差距(参见56)。

Browsix-Wasm 和 Browsix-SPEC 可在 https://browsix.org 获取。

图1:不是你想象的那么快:WebAssembly 与原生代码性能对比分析

图 2:在浏览器中运行 SPEC 基准测试的框架。粗体组件为新增或重大修改部分(§3))。

2 从 Browsix 到 Browsix-Wasm

Browsix [29 在浏览器内模拟 Unix 内核,并包含一个基于 Emscripten [3933]),可将原生程序编译为 JavaScript。二者结合使原生程序(C/C++/Go语言)能在浏览器中运行,并自由调用管道、进程、文件系统等操作系统服务。但Browsix存在两大限制:其一,它将原生代码编译为JavaScript而非WebAssembly;其二,其内核存在显著性能问题。尤其在 Browsix 中,若干常见系统调用存在极高开销,导致难以比较 Browsix 运行环境与原生环境下的程序性能。为此我们构建了名为 Browsix-Wasm 的全新浏览器内内核,该内核支持 WebAssembly 程序并消除了 Browsix 的性能瓶颈。

Emscripten 运行时修改

Browsix 通过修改 Emscripten 编译器,使进程(在 WebWorkers 中运行)能够与 Browsix 内核(在页面主线程运行)通信。由于 Browsix 将原生程序编译为 JavaScript,实现方式相对简单:每个进程的内存都是与内核共享的缓冲区(SharedArrayBuffer),因此系统调用可直接读写进程内存。然而此方案存在两大缺陷:首先,它排除了按需扩展堆的可能——共享内存必须预先分配足够大的空间,以满足进程生命周期内应用程序的最高堆容量需求。其次,JavaScript 上下文(如主上下文和每个 Web 工作者上下文)的堆大小存在固定限制,当前在 Google Chrome 中约为 2.2 GB [6]。该上限对多进程运行构成严重限制:若每个进程预留500MB堆内存,Browsix最多只能同时运行四个进程。更深层的问题在于WebAssembly内存无法在WebWorkers间共享,且不支持原子API——而Browsix进程正是通过该API等待系统调用。

Browsix-Wasm采用不同的进程-内核通信方案,其速度也优于Browsix方案。该方案通过修改Emscripten运行时系统,为每个进程创建一个64MB辅助缓冲区,该缓冲区与内核共享但独立于进程内存。由于该辅助缓冲区采用SharedArrayBuffer实现,Browsix-Wasm进程与内核可通过Atomic API进行通信。当系统调用涉及进程堆中的字符串或缓冲区(如writev或stat)时,其运行时系统会将数据从进程内存复制到共享缓冲区,并向内核发送包含辅助内存中复制数据位置的消息。同样地,当系统调用向辅助缓冲区写入数据时(如read操作),其运行时系统会将数据从共享缓冲区复制到指定内存地址的进程内存中。此外,若系统调用指定内核写入进程内存中的缓冲区(如read操作),运行时会分配辅助内存中对应的缓冲区并传递给内核。当系统调用涉及读写超过64MB的数据时,Browsix-Wasm会将该调用拆分为多个子调用,确保每次操作最多处理64MB数据。这些内存复制操作的开销远低于系统调用整体成本——后者涉及进程与内核JavaScript上下文间的消息传递。我们在§4.2.1中证明Browsix-Wasm的开销可忽略不计。

性能优化

在构建Browsix-Wasm并进行初步性能评估时,我们发现了Browsix内核部分存在的若干性能问题。若不加以解决,这些问题将危及WebAssembly与原生代码性能对比的有效性。最严重的问题出现在Browsix/Browsix-Wasm内置的共享文件系统组件BrowserFS中。最初,每次对文件进行追加操作时,BrowserFS都会分配一个更大的新缓冲区,并将原有内容与新内容复制到新缓冲区。即使是小规模追加操作也会导致显著的性能下降。现在,当文件的缓冲区需要额外空间时,BrowserFS会将缓冲区至少扩展4KB。仅此一项改动就使464.h264ref基准测试在Browsix中的耗时从25秒缩短至1.5秒以内。我们还实施了一系列优化,全面降低Browsix-Wasm的开销。在管道内核实现中,我们同样通过减少分配次数和复制量实现了类似(虽不那么显著)的性能提升。

3Browsix-SPEC

为可靠执行WebAssembly基准测试并捕获性能计数器数据,我们开发了Browsix-SPEC。该工具协同Browsix-Wasm管理浏览器实例创建、基准测试资源(如编译后的WebAssembly程序和测试输入)分发、性能计数器数据记录进程启动,以及基准测试输出验证。

我们使用Browsix-SPEC运行三套基准测试套件评估WebAssembly性能:SPEC CPU2006、SPEC CPU2017和PolyBenchC。这些基准测试使用Clang 4.0编译为本机代码,并通过Browsix-Wasm编译为WebAssembly。我们未对Chrome或Firefox进行任何修改,浏览器均在启用标准沙箱隔离功能的状态下运行。Browsix-Wasm基于标准Web平台特性构建,无需直接访问主机资源——基准测试通过标准HTTP请求向Browsix-SPEC发起调用。

3.1 Browsix-SPEC基准测试执行

2 展示了运行基准测试(如 Chrome 中的 401.bzip2)时 Browsix-SPEC 的核心组件。首先(1),Browsix-SPEC基准测试框架通过WebBrowser自动化工具Selenium启动新浏览器实例。2(2)浏览器通过HTTP从测试框架加载页面HTML、框架JS及Browsix-Wasm内核JS。(3) 测试框架JS初始化Browsix-Wasm内核,并启动新进程执行runspec shell脚本(未在图2。runspec随后调用标准specinvoke(未展示),该函数基于SPEC 2006提供的C源代码编译而成。specinvoke从Browsix-Wasm文件系统读取speccmds.cmd文件,并以正确参数启动401.bzip2程序。(4) WebAssembly模块实例化后,但在基准测试主函数调用前,Browsix-Wasm用户空间运行时会向Browsix-SPEC发起XHR请求以开始记录性能计数器统计数据。(5) 基准测试框架定位对应Web Worker进程401.bzip2的Chrome线程,并将性能监控工具附加至该进程。(6) 基准测试结束时,Browsix-Wasm用户空间运行时向测试框架发起最终XHR请求以终止性能记录进程。当runspec程序退出(可能已多次调用测试二进制文件)后,框架通过JS POST(7)将SPEC结果目录的tar归档文件上传至Browsix-SPEC。Browsix-SPEC接收完整结果归档后,将其解压至临时目录,并使用SPEC 2006附带的cmp工具验证输出。最后,Browsix-SPEC终止浏览器进程并记录基准测试结果。

图2:不是你想象的那么快:WebAssembly 与原生代码性能对比分析

(a)

图3:不是你想象的那么快:WebAssembly 与原生代码性能对比分析

(b) 图3:使用Browsix-Wasm和Browsix-SPEC在Chrome与Firefox中执行时,PolyBenchC及SPEC CPU基准测试编译为WebAssembly后的性能表现(相对于原生环境)。SPEC CPU基准测试整体开销高于PolyBenchC套件,表明WebAssembly与原生环境存在显著性能差距。

4评估

我们使用Browsix-Wasm和Browsix-SPEC通过三套基准测试套件评估WebAssembly的性能:SPEC CPU2006、SPEC CPU2017和PolyBenchC。我们纳入PolyBenchC基准测试以与WebAssembly原始论文[18]进行对比,但认为这些测试无法代表典型工作负载。SPEC基准测试更具代表性,且要求Browsix-Wasm成功运行。所有基准测试均在配备超线程技术的6核英特尔至强E5-1650 v3处理器上运行,该系统搭载64GB内存,运行Ubuntu 16.04操作系统及Linux内核v4.4.0。所有基准测试均采用两款主流浏览器:Google Chrome 74.0与Mozilla Firefox 66.0。测试程序分别通过Clang 4.03编译为原生代码,并使用Browsix-Wasm(基于Clang 4.0的Emscripten实现)编译为WebAssembly。4 每项测试执行五次。我们报告所有运行时间的平均值及其标准误差。所测执行时间为程序启动时(即WebAssembly即时编译完成后)与程序结束时墙钟时间的差值。

4.1 PolyBenchC 基准测试

Haas 等人在 [18] 中采用 PolybenchC 评估 WebAssembly 实现,因其基准测试不涉及系统调用。正如我们先前论证的,PolybenchC基准测试是小型科学计算内核,通常用于评估多面体优化技术,无法代表大型应用程序。尽管如此,使用Browsix-Wasm运行PolybenchC仍具有重要价值,因为它证明了我们的系统调用基础设施不存在任何开销。图3(a)展示了Browsix-Wasm与原生环境下PolyBenchC基准测试的执行时间。我们成功复现了原始WebAssembly论文[18]中绝大多数结果。实验表明Browsix-Wasm引入的开销极低:平均仅0.2%,最高不超过1.2%。

4.2 SPEC基准测试

我们现采用SPEC CPU2006和SPEC CPU2017(新版C/C++基准测试及速度基准测试)中的C/C++测试案例评估Browsix-Wasm,这些测试大量使用系统调用。我们剔除了四个数据点:这些测试要么无法编译为 WebAssembly⁵,要么分配的内存超过 WebAssembly 的限制。⁶ 表 1 展示了在 Chrome 和 Firefox 浏览器中使用 Browsix-Wasm 运行 SPEC 基准测试,以及原生运行时的绝对执行时间。

除429.mcf和433.milc外,所有基准测试中WebAssembly性能均逊于原生代码。在Chrome中,WebAssembly最高开销达原生代码的2.5倍,但15项测试中有7项运行时间控制在原生代码的1.5倍内。在 Firefox 中,WebAssembly 的运行时间不超过原生环境的 2.08 倍,且 15 个基准测试中有 7 个的运行时间控制在原生环境的 1.5 倍以内。平均而言,WebAssembly 在 Chrome 中的运行速度比原生环境慢 1.55 倍,在 Firefox 中慢 1.45 倍。表2展示了使用Clang和Chrome编译SPEC基准测试所需的时间。(据我们所知,Firefox无法报告WebAssembly编译时间。)在所有情况下,编译时间相对于执行时间都微不足道。然而Clang编译器的速度比WebAssembly编译器慢几个数量级。最后需注意:Clang编译的是C++源代码基准测试,而Chrome编译的是比C++更简单的WebAssembly格式。

表1:原生编译(Clang)与WebAssembly编译(Chrome和Firefox)的SPEC CPU基准测试执行时间明细(5次运行平均值);所有时间单位为秒。WebAssembly的平均延迟倍数为Chrome 1.53倍,Firefox 1.54倍。

表2:Clang 4.0与WebAssembly(Chrome)编译SPEC CPU基准测试耗时(5次运行平均值);单位均为秒。

4.2.1 Browsix-Wasm开销分析

必须排除我们报告的性能下降是由Browsix-Wasm实现方案导致的可能性。尤其需要注意的是,Browsix-Wasm在不修改浏览器的情况下实现系统调用,而系统调用涉及数据复制(参见§2),这可能造成较高开销。为量化Browsix-Wasm的开销,我们对其系统调用进行了仪器化处理,以测量在Browsix-Wasm中消耗的全部时间。图4展示了Firefox使用SPEC基准测试时Browsix-Wasm的耗时占比。在15项基准测试中,有14项的开销低于0.5%。最高开销为1.2%。平均而言,Browsix-Wasm的开销仅为0.2%。因此我们得出结论:Browsix-Wasm的开销可忽略不计,不会显著影响WebAssembly环境下程序的性能计数器结果。

图4:不是你想象的那么快:WebAssembly 与原生代码性能对比分析

图4:Firefox中编译为WebAssembly的SPEC基准测试中,Browsix-Wasm调用所耗时间占比(%)。Browsix-Wasm平均开销仅为0.2%。

4.2.2 WebAssembly与asm.js对比

WebAssembly早期研究的核心主张是其性能显著优于asm.js。我们现通过SPEC基准测试验证该论断。为此,我们修改了Browsix-Wasm以支持编译为asm.js的进程。另一种方案是使用原始Browsix对asm.js进程进行基准测试。然而如前所述,Browsix存在性能缺陷,可能危及测试结果的有效性。图5 展示了在 Chrome 和 Firefox 浏览器中,WebAssembly 相对于 asm.js 的 SPEC 基准测试加速率。WebAssembly 在两款浏览器中均优于 asm.js:Chrome 平均加速率为 1.54 倍,Firefox 为 1.39 倍。

鉴于Chrome与Firefox性能差异显著,在图6 中,我们通过选择 WebAssembly 性能最佳的浏览器和 asm.js 性能最佳的浏览器(即可能为不同浏览器)来展示每个基准测试的速度提升。这些结果表明 WebAssembly 的性能始终优于 asm.js,平均速度提升为 1.3 倍。Haas等人[18]也发现,使用PolyBenchC测试时,WebAssembly相较asm.js平均提升1.3倍性能。

图5:不是你想象的那么快:WebAssembly 与原生代码性能对比分析

图5:Chrome和Firefox中asm.js与WebAssembly的相对运行时间。WebAssembly在Chrome中比asm.js快1.54倍,在Firefox中快1.39倍。

图6:不是你想象的那么快:WebAssembly 与原生代码性能对比分析

图6:asm.js相对于WebAssembly最佳运行时间的相对表现。WebAssembly比asm.js快1.3倍。

5 案例研究:矩阵乘法

本节通过实现矩阵乘法的C函数如图[7(a)]所示。该函数接收三个矩阵作为参数,并将A(NI×NK)与B(NK×NJ)的乘积存储于C(NI×NJ)中,其中NI、NK、NJ为程序中定义的常量。

在 WebAssembly 中,该函数在 Chrome 和 Firefox 浏览器中运行时,相较原生代码慢了 2×–3.4×(图 8。我们使用-O2编译该函数并禁用自动向量化,因为WebAssembly不支持向量化指令。

void matmul (int C[NI][NJ],
             int A[NI][NK],
             int B[NK][NJ]) {
  for (int i = 0; i < NI; i++) {
    for (int k = 0; k < NK; k++) {
      for (int j = 0; k < NJ; j++) {
        C[i][j] += A[i][k] * B[k][j];
      }
    }
  }
}

(a)matmul source code in C.

xor  r8d, r8d           #i <- 0
L1:                     #start first loop 
  mov  r10, rdx
  xor  r9d, r9d         #k <- 0
  L2:                   #start second loop 
    imul  rax, 4*NK, r8
    add   rax, rsi
    lea   r11, [rax + r9*4]
    mov   rcx, -NJ     #j <- -NJ 
    L3:                 #start third loop 
      mov   eax, [r11]
      mov   ebx, [r10 + rcx*4 + 4400]
      imul  ebx, eax
      add   [rdi + rcx*4 + 4*NJ], ebx 
      add   rcx, 1      #j <- j + 1 
    jne L3              #end third loop       
    add   r9,  1        #k <- k + 1
    add   r10, 4*NK
    cmp   r9,  NK
  jne L2                #end second loop 
  add  r8,  1           #i <- i + 1
  add  rdi, 4*NJ
  cmp  r8,  NI
jne L1                  #end first loop 
pop  rbx
ret

(b)Native x86-64 code for matmul generated by Clang.

1mov [rbp-0x28],rax                    
2mov [rbp-0x20],rdx                    
3mov [rbp-0x18],rcx                    
4xor edi,edi               #i <- 0
5jmp L1’                               
6L1:                       #start first loop
7  mov ecx,[rbp-0x18]                 
8  mov edx,[rbp-0x20]                
9  mov eax,[rbp-0x28]                
10  L1’:
11  imul r8d,edi,0x1130
12  add r8d,eax
13  imul r9d,edi,0x12c0
14  add r9d,edx
15  xor r11d,r11d           #k <- 0
16  jmp L2’                           
17  L2:                     #start second loop
18    mov ecx,[rbp-0x18]            
19    L2’:
20    imul r12d,r11d,0x1130
21    lea r14d,[r9+r11*4]
22    add r12d,ecx
23    xor esi,esi           #j <- 0
24    mov r15d,esi
25    jmp L3’                       
26    L3:                   #start third loop
27      mov r15d,eax           
28      L3’:
29      lea eax,[r15+0x1]   #j <- j + 1 
30      lea edx,[r8+r15*4]
31      lea r15d,[r12+r15*4]
32      mov esi,[rbx+r14*1]
33      mov r15d,[rbx+r15*1]
34      imul r15d,esi
35      mov ecx,[rbx+rdx*1]   
36      add ecx,r15d
37      mov [rbx+rdx*1],ecx    
38      cmp eax,NJ          #j < NJ 
39    jnz L3                #end third loop 
40  add r11,0x1             #k++
41  cmp r11d,NK             #k < NK
42  jnz L2                  #end second loop 
43add edi,0x1               #i++
44cmp edi,NI                #i < NI
45jnz L1                    #end first loop 
46retl

(c)x86-64 code JITed by Chrome from WebAssembly matmul.

图7:原生代码实现的矩阵乘法比Chrome JIT编译的代码更短,寄存器压力更小,分支更少。§6 表明此类低效现象普遍存在,导致 SPEC CPU 基准测试套件整体性能下降。

图7:不是你想象的那么快:WebAssembly 与原生代码性能对比分析

图8:Chrome和Firefox浏览器中WebAssembly在不同矩阵尺寸下的性能表现(相对于原生代码)。WebAssembly始终比原生代码慢2倍至3.4倍。

7(b)展示了由clang-4.0生成的矩阵乘法函数原生代码。参数通过rdi、rsi和rdx寄存器传递给函数,这符合System V AMD64 ABI调用约定[9]的规定。第7(b)行7(b) 构成第一个循环的主体,其中迭代器 i 存储在 r8d 中。第 7(b)7(b) 包含第二个循环的主体,其中迭代器 k 存储在 r9d 中。行 7(b)7(b) 构成第三个循环的主体,其中迭代器 j 存储在 rcx 中。Clang 能够通过将 rcx 初始化为 −�� 来消除内部循环中的 cmp 指令,并在每行迭代时递增 rcx 7(b),并使用 jne 指令检测状态寄存器的零标志位(当 rcx 为 0 时该标志位设为 1)。

7(c)展示了Chrome为WebAssembly编译版本的矩阵乘法生成的x86-64代码。该代码略有修改——为便于展示,已移除生成的空操作指令。函数参数遵循Chrome调用约定,通过rax、rcx和rdx寄存器传递。在第13中,由于寄存器溢出,寄存器 rax、rdx 和 rcx 的内容存储在栈上,具体见第 79。第 745 构成第一个循环的主体,其中迭代器 i 存储在 edi 中。第 18424239 构成第三个循环的主体,其中迭代器 j 存储于 eax 寄存器。为避免第 79中,在第一个循环的第一轮迭代时,第5。同样地,在第二和第三个循环中,第16行和第25行

5.1差异

相较于 Clang 生成的原生代码,Chrome 即时编译的原生代码包含更多指令,面临更高的寄存器压力,并存在额外分支。

5.1.1 代码体积增大

Chrome生成的代码指令数(图7(c)仅含28条指令(含空指令),而Clang生成的代码(图7(b))仅包含28条指令。Chrome糟糕的指令选择算法是导致代码体积增大的原因之一。

此外,Chrome并未充分利用x86指令集的所有可用内存寻址模式。在图7(b)中,Clang在第7(b)行 使用寄存器寻址模式,在同一操作中完成内存地址的加载与写入。另一方面,Chrome 则将地址加载到 ecx 中,将操作数加到 ecx 上,最后将 ecx 存储在该地址上,需要 3 条指令而不是 1 条指令(第 3537

5.1.2 寄存器压力增加

Clang生成的代码(图7(b)未产生溢出,仅使用10个寄存器。而Chrome生成的代码(图7(c)使用了 13 个通用寄存器——即所有可用寄存器(r13 和 r10 被 V8 预留)。如第5.1.1节,放弃使用加法指令的寄存器寻址模式需要使用临时寄存器。所有这些寄存器效率问题累积起来,导致第1-3行向堆栈溢出三个寄存器。存储在堆栈中的值在第79 以及第 18

5.1.3额外分支

Clang(图7(b)) 通过反转循环计数器生成单分支循环代码(第 7(b)) 通过反转循环计数器生成单分支循环代码(第 7(b)) 则生成更直观的代码,需在循环起始处添加条件跳转。此外,Chrome 为避免循环首轮因寄存器溢出导致的内存加载,额外生成跳转指令。例如第 5避免了第7行9

6性能分析

perf Event Wasm Summary
all-loads-retired (r81d0) (Figure 9(a)) Increased register
all-stores-retired (r82d0) (Figure 9(b)) pressure
branches-retired (r00c4) (Figure 9(c)) More branch
conditional-branches (r01c4) (Figure 9(d)) statements
instructions-retired (r1c0) (Figure 9(e)) Increased code size
cpu-cycles (Figure 9(f))
L1-icache-load-misses (Figure 10)
表3:性能计数器揭示WebAssembly代码生成的特定问题。当使用原始PMU事件描述符时,以r​�标注

图8:不是你想象的那么快:WebAssembly 与原生代码性能对比分析

(a)所有加载指令已完成

图9:不是你想象的那么快:WebAssembly 与原生代码性能对比分析

(b)所有存储指令已完成

图10:不是你想象的那么快:WebAssembly 与原生代码性能对比分析

(c)分支指令执行

图11:不是你想象的那么快:WebAssembly 与原生代码性能对比分析

(d)条件分支

图12:不是你想象的那么快:WebAssembly 与原生代码性能对比分析

(e)已废弃指令

图13:不是你想象的那么快:WebAssembly 与原生代码性能对比分析

(f)CPU周期 图9:WebAssembly相对于原生代码的处理器性能计数器采样值

我们使用Browsix-SPEC记录系统中所有支持的性能计数器数据,涵盖编译为WebAssembly并在Chrome/Firefox中执行的SPEC CPU基准测试,以及编译为原生代码的SPEC CPU基准测试参见第[3]节中列出的性能计数器。

3 列出了本文使用的性能计数器,并总结了 Browsix-Wasm 性能相较于原生代码对这些计数器的影响。我们将利用这些结果阐释 WebAssembly 相较于原生代码的性能开销。我们的分析表明,第5节所述的低效问题具有普遍性,并导致SPEC CPU基准测试套件整体性能下降。

6.1 寄存器压力增大

本节重点分析两个体现寄存器压力增大效应的性能计数器。图9(a)展示了Chrome和Firefox中WebAssembly编译的SPEC基准测试完成的 加载 指令数量,并与原生代码完成的加载指令数量进行对比。同样,图9(b)显示了已退出的 存储 指令数量。需注意,”完成”指令指已离开指令流水线且结果正确、在体系结构状态中可见的指令(即非投机性指令)。

Chrome生成的代码完成的加载指令数量是原生代码的2.02倍,存储指令数量是原生代码的2.30倍。Firefox生成的代码完成的加载指令数量是原生代码的1.92倍,存储指令数量是原生代码的2.16倍。这些结果表明,WebAssembly编译的SPEC CPU基准测试存在寄存器压力增大导致内存引用增加的问题。下文将阐述寄存器压力增大的原因。

6.1.1 预留寄存器

在Chrome中,矩阵乘法会产生三个寄存器溢出,但未使用两个x86-64寄存器:r13和r10(图7(c),第79)。这是因为Chrome预留了这两个寄存器。7 对于JavaScript垃圾回收器,Chrome始终将r13保留为指向GC根数组的指针。此外,Chrome将r10和xmm13用作专用暂存寄存器。同样,Firefox将r15保留为指向堆起始地址的指针,而r11和xmm15则是JavaScript的暂存寄存器。8 这些寄存器均不可供WebAssembly代码使用。

6.1.2 寄存器分配效率低下

除可用寄存器数量受限外,Chrome 和 Firefox 对现有寄存器的分配效率同样欠佳。例如Chrome生成的矩阵乘法代码使用12个寄存器,而Clang生成的原生代码仅用10个寄存器(参见5.1.2)。Firefox 和 Chrome 寄存器使用量的增加,是由于它们使用了快速但效率不高的寄存器分配器。Chrome和Firefox均采用线性扫描寄存器分配器 [36],而Clang采用贪婪图着色寄存器分配器[3],后者始终能生成更优代码。

6.1.3 x86寻址模式

x86-64指令集为每个操作数提供了多种寻址模式,包括 寄存器 模式(指令从寄存器读取数据或向寄存器写入数据),以及 寄存器间接直接偏移 寻址等内存地址模式(操作数位于内存地址处,指令可读取或写入该地址)。代码生成器可通过采用后者模式避免不必要的寄存器压力。然而Chrome并未充分利用这些模式。例如其生成的矩阵乘法代码中,加法指令未使用寄存器间接寻址模式(参见5.1.2节),导致不必要的寄存器压力。

6.2额外分支指令

本节重点分析两个用于统计分支指令执行次数的性能计数器。图9(c) 展示了 WebAssembly 完成的分支指令数量,并与原生代码完成的分支指令数量进行对比。同样,图9(d)显示了已退出的 条件 分支指令数量。在Chrome中,无条件和有条件分支指令的执行量分别高出1.75倍和1.65倍;而在Firefox中,该比例分别为1.65倍和1.62倍。这些结果表明所有SPEC CPU基准测试都会产生额外分支,下文将阐述原因。

6.2.1 循环中的额外跳转语句

与矩阵乘法(参见第5.1.3节)中所述,Chrome 为循环生成多余的跳转语句,导致其分支指令数量远超 Firefox。

6.2.2 函数调用中的栈溢出检查

某 WebAssembly 程序通过全局变量追踪当前栈容量,每次函数调用时该变量值递增。程序员可定义程序的最大栈容量。为防止程序栈溢出,Chrome 和 Firefox 均在每个函数开头添加栈检查,用于检测当前栈容量是否低于最大值。这些检查包含额外的比较和条件跳转指令,且每次函数调用都必须执行。

6.2.3 函数表索引检查

WebAssembly会动态检查所有间接调用,确保目标为有效函数,且运行时函数类型与调用点指定类型一致。在 WebAssembly 模块中,函数表存储函数列表及其类型,WebAssembly 生成的代码通过函数表实现这些检查。调用 C/C++ 中的函数指针和虚函数时需要执行这些检查。这些检查导致额外的比较和条件跳转指令,在每次间接函数调用前执行。

6.3 代码体积增大

Chrome和Firefox生成的代码体积远大于Clang生成的代码。我们通过三个性能计数器衡量此影响:(i) 图9(e) 展示了编译为 WebAssembly 并在 Chrome/Firefox 中执行的基准测试中, 指令完成数 相对于原生代码指令完成数的比率。同样,图9(f) 展示了编译为WebAssembly的基准测试所消耗的 CPU周期 相对数量,而图10 展示了 L1指令缓存加载失效 的相对数量。

9(e)显示,Chrome平均执行指令量是原生代码的1.80倍,Firefox平均执行指令量是原生代码的1.75倍。由于指令选择不佳,劣质寄存器分配器导致更多寄存器溢出(第6.1节),以及额外的分支语句参见第6.2节,WebAssembly 生成的代码体积大于原生代码,导致执行指令数量增加。指令执行量的增长在图10。平均而言,Chrome的指令缓存缺失次数是原生代码的2.83倍,Firefox的L1指令缓存缺失次数则是原生代码的2.04倍。更多缓存未命中意味着更多CPU周期耗费在等待指令获取上。

我们注意到一个异常现象:尽管429.mcf在Chrome中的指令完成量是原生代码的1.6倍,在Firefox中是原生代码的1.5倍,但其运行速度却 快于 原生代码。图3(b)显示,其相对于原生代码的性能下降幅度在Chrome中为0.81倍,在Firefox中为0.83倍。这种异常现象直接归因于其较低的一级指令缓存缺失率。429.mcf包含主循环,且循环中大部分指令可容纳于一级指令缓存。同样地,433.milc的性能更优也源于较少L1指令缓存缺失。而在450.soplex中,由于执行了多个虚函数导致间接函数调用增多,Chrome和Firefox的L1指令缓存缺失次数比原生代码高出4.6倍。

 

Performance Counter Chrome Firefox
all-loads-retired 2.02

×\times

1.92

×\times

all-stores-retired 2.30

×\times

2.16

×\times

branch-instructions-retired 1.75

×\times

1.65

×\times

conditional-branches 1.65

×\times

1.62

×\times

instructions-retired 1.80

×\times

1.75

×\times

cpu-cycles 1.54

×\times

1.38

×\times

L1-icache-load-misses 2.83

×\times

2.04

×\times

表4:基于WebAssembly的SPEC基准测试中性能计数器增长的几何平均值。

6.4讨论

值得探讨的是,本文发现的性能问题是否具有根本性。我们认为其中两项问题并非如此:即通过改进实现方案可缓解这些问题。当前WebAssembly实现采用寄存器分配器(§6.1.2),其性能表现逊于Clang的同类组件。但Clang这类离线编译器能投入更多时间生成更优代码。6 性能分析 ‣ 不是你想象的那么快:WebAssembly 与原生代码性能对比分析”)) 表现逊于 Clang 的同类工具。但离线编译器如 Clang 可投入更多时间生成更优代码,而 WebAssembly 编译器必须具备足够速度以支持在线运行。因此,其他即时编译器采用的解决方案(如进一步优化热点代码)在此场景中同样适用 [19, 32

我们发现的另外四个问题似乎源于WebAssembly的设计限制:栈溢出检查(§6.2.2),间接调用检查(§6.2.3),以及保留寄存器(§6.1.1) 均存在运行时开销并导致代码体积增大(§6.3)。遗憾的是,这些检查对WebAssembly的安全性保障不可或缺。若对WebAssembly进行重构,为内存和函数指针提供更丰富的类型[23],或许能在编译时完成部分检查,但这可能增加生成WebAssembly的编译器实现复杂度。最后,浏览器中的 WebAssembly 实现必须与高性能 JavaScript 实现互操作,这可能带来额外限制。例如,当前 JavaScript 实现会为自身保留若干寄存器,从而加剧 WebAssembly 的寄存器压力。

7相关工作

WebAssembly的前身

此前曾有多种在浏览器中执行原生代码的尝试,但均未满足WebAssembly的所有设计标准。

ActiveX [13] 允许网页嵌入签名 x86 库,但这些二进制文件可无限制访问 Windows API。相比之下,WebAssembly 模块处于沙箱环境。ActiveX 现已成为弃用技术。

原生客户端 [37, 11](NaCl)为网页应用添加了包含平台特定机器码的模块。NaCl引入沙箱技术,使平台特定机器码能以接近原生速度运行。由于NaCl依赖机器码的静态验证,要求代码生成器遵循特定模式,因此仅支持浏览器中x86、ARM和MIPS指令集的子集。为解决NaCl固有的可移植性问题,可移植NaCl(PNaCl)[14]采用LLVM位码作为二进制格式。然而PNaCl在紧凑性上并未显著优于NaCl,且仍暴露编译器和/或平台特有的细节(如调用堆栈布局)。这两种方案现均已被弃用,WebAssembly成为主流替代方案。

asm.js是JavaScript的子集,旨在高效编译为原生代码。它通过类型强制转换规避JavaScript的动态类型系统。由于asm.js是JavaScript的子集,要为其添加64位整数等原生特性,首先需要扩展JavaScript本身。相较于asm.js,WebAssembly具备多重优势:(i) 基于轻量级表示形式,WebAssembly二进制文件比JavaScript源代码更紧凑;(ii) WebAssembly更易于验证; (iii) WebAssembly提供类型安全与隔离性的形式化保证,(iv)实践证明WebAssembly性能优于asm.js。

WebAssembly作为栈式机器,其架构类似于Java虚拟机[21]。但WebAssembly与这些平台存在显著差异:例如它不支持对象类型,也不支持非结构化控制流。

WebAssembly规范定义了其操作语义和类型系统。该证明通过Isabelle定理证明器实现机械化,该机械化工作发现了规范[35]中的若干问题并予以解决。RockSalt [22] 是针对 NaCl 的类似验证项目。该项目在 Coq 中实现了 NaCl 验证工具链,并针对 NaCl 支持的 x86 指令子集模型证明了其正确性。

基于性能计数器的SPEC基准测试分析

多篇论文采用性能计数器分析SPEC基准测试。Panda等人[26]]通过统计技术分析SPEC CPU2017基准测试,以识别不同基准测试间的相似性。Phansalkar等人对SPEC CPU2006进行了类似研究[27]。Limaye和Adegija揭示了SPEC CPU2006与SPEC CPU2017之间的工作负载差异 [20]。

8结论

本文首次对WebAssembly进行了全面性能分析。我们开发了Browsix-Wasm(Browsix的重要扩展)和Browsix-SPEC(支持深度性能分析的测试框架),使Chrome和Firefox浏览器能够运行SPEC CPU2006与CPU2017基准测试。我们发现,在 SPEC 基准测试中,WebAssembly 相对于原生的平均性能下降幅度为:Chrome 浏览器 1.55 倍,Firefox 浏览器 1.45 倍,峰值性能下降幅度分别为 Chrome 浏览器的 2.5 倍和 Firefox 浏览器的 2.08 倍。我们确定了这些性能差距的原因,并为未来的优化工作提供了可操作的指导。

致谢

感谢审稿人及导师Eric Eide提供的建设性意见。本研究部分由美国国家科学基金会1439008号与1413985号项目资助。

参考文献

  • [1] Blazor. https://blazor.net/. [在线; 访问日期:2019年1月5日].
  • [2] 从Rust编译为WebAssembly. https://developer.mozilla.org/en-US/docs/WebAssembly/Rust_to_wasm. [在线; 访问于2019年1月5日].
  • [3] LLVM 参考手册。 https://llvm.org/docs/CodeGenerator.html
  • [4] NaCl 与 PNaCl。 https://developer.chrome.com/native-client/nacl-and-pnacl。 [在线;访问于2019年1月5日]。
  • [5] PolyBenchC:多面体基准测试套件。 http://web.cs.ucla.edu/~pouchet/software/polybench/。 [在线;访问于2017年3月14日]。
  • [6] 提高Chrome JS堆内存限制? – Stack Overflow。 https://stackoverflow.com/questions/43643406/raise-chrome-js-heap-limit。 [在线;访问于2019年1月5日]
  • [7] 用例。 https://webassembly.org/docs/use-cases/
  • [8] WebAssembly。 https://webassembly.org/。[在线;访问于2019年1月5日]。
  • [9] System V应用程序二进制接口AMD64架构处理器补充说明。 https://software.intel.com/sites/default/files/article/402129/mpx-linux64-abi.pdf,2013年。
  • [10] Steve Akinyemi. 一份精选语言列表,这些语言可直接编译为 WebAssembly 或在其虚拟机中运行。 https://github.com/appcypher/awesome-wasm-langs. [在线;访问日期:2019年1月5日].
  • [11] Jason Ansel, Petr Marchenko, Úlfar Erlingsson, Elijah Taylor, Brad Chen, Derek L. Schuff, David Sehr, Cliff L. Biffle, and Bennet Yee. 语言无关的即时编译与自修改代码沙箱机制. 收录于第32届ACM SIGPLAN编程语言设计与实现会议论文集(PLDI '11),第355–366页。ACM出版社,2011年。
  • [12] Michael Bebenita, Florian Brandner, Manuel Fahndrich, Francesco Logozzo, Wolfram Schulte, Nikolai Tillmann, and Herman Venter. SPUR:基于跟踪的CIL即时编译器. 收录于ACM面向对象编程系统语言与应用国际会议论文集(OOPSLA ’10),第708–725页. ACM,2010年。
  • [13] David A Chappell. 《ActiveX与OLE解析》. 微软出版社,1996年。
  • [14] Alan Donovan, Robert Muth, Brad Chen, and David Sehr. PNaCl:可移植原生客户端可执行文件. https://css.csail.mit.edu/6.858/2012/readings/pnacl.pdf,2010年。
  • [15] 布伦丹·艾希。从ASM.JS到WebAssembly。https://brendaneich.com/2015/06/from-asm-js-to-webassembly/,2015年。 [在线;访问于2019年1月5日]
  • [16] Eric Elliott. 什么是WebAssembly? https://tinyurl.com/o5h6daj,2015年。 [在线;访问于2019年1月5日]
  • [17] Andreas Gal, Brendan Eich, Mike Shaver, David Anderson, David Mandelin, Mohammad R. Haghighat, Blake Kaplan, Graydon Hoare, Boris Zbarsky, Jason Orendorff, Jesse Ruderman, Edwin W. Smith, Rick Reitmaier, Michael Bebenita, Mason Chang, and Michael Franz. 基于跟踪的动态语言即时类型特化. 载于第30届ACM SIGPLAN编程语言设计与实现会议论文集(PLDI '09),第465–478页. ACM, 2009.
  • [18] Andreas Haas, Andreas Rossberg, Derek L. Schuff, Ben L. Titzer, Michael Holman, Dan Gohman, Luke Wagner, Alon Zakai, JF Bastien. 《WebAssembly加速网页运行》。收录于第38届ACM SIGPLAN编程语言设计与实现会议论文集(PLDI 2017),第185–200页。ACM,2017年。
  • [19] Thomas Kotzmann, Christian Wimmer, Hanspeter Mössenböck, Thomas Rodriguez, Kenneth Russell, David Cox. 《Java 6 HotSpot客户端编译器的设计》. 《ACM架构与代码优化汇刊》, 5(1):7:1–7:32, 2008年.
  • [20] 安库尔·利迈与托西隆·阿德吉巴. SPEC CPU2017基准测试套件的工作负载特征分析. 2018年IEEE系统与软件性能分析国际研讨会(ISPASS)论文集,第149–158页,2018年.
  • [21] 蒂姆·林德霍姆、弗兰克·耶林、吉拉德·布拉查与亚历克斯·巴克利. 《Java虚拟机规范:Java SE 8版》. Addison-Wesley专业出版,第1版,2014年.
  • [22] 格雷格·莫里塞特、谭刚、约瑟夫·塔萨罗蒂、让-巴蒂斯特·特里斯坦与爱德华·甘. RockSalt:面向x86架构的更优、更快、更强的SFI实现。收录于第33届ACM SIGPLAN编程语言设计与实现会议论文集(PLDI '12),第395–404页。ACM出版社,2012年。
  • [23] Greg Morrisett, David Walker, Karl Crary, and Neal Glew. 从系统F到类型化汇编语言. ACM程序语言与系统汇刊, 21(3):527–568, 1999.
  • [24] Richard Musiol. 《从Go到JavaScript的编译器:在浏览器中运行Go代码》. https://github.com/gopherjs/gopherjs, 2016. [在线; 访问于2019年1月5日].
  • [25] George C. Necula, Scott McPeak, Shree P. Rahul, and Westley Weimer. CIL:用于C程序分析与转换的中间语言及工具集. 收录于R. Nigel Horspool编著《编译器构造》, 第213–228页. Springer出版社,2002年。
  • [26] Reena Panda, Shuang Song, Joseph Dean, and Lizy K. John. 十年之盼:SPEC CPU 2017是否拓展了性能视野? 收录于2018年IEEE高性能计算机体系结构国际研讨会(HPCA),第271–282页,2018年。
  • [27] 阿希什·潘萨尔卡、阿杰·乔希、莉齐·K·约翰。《SPEC CPU2006基准测试套件中的冗余与应用平衡分析》。载于第34届国际计算机体系结构年会论文集(ISCA '07),第412–423页。ACM出版社,2007年。
  • [28] Bobby Powers, John Vilk, 和 Emery D. Berger. Browsix:浏览器标签页中的Unix系统. https://browsix.org.
  • [29] Bobby Powers, John Vilk, 和 Emery D. Berger. Browsix:弥合Unix与浏览器之间的鸿沟. 载于《第二十二届编程语言与操作系统架构支持国际会议论文集》(ASPLOS ’17),第253–266页。ACM,2017年。
  • [30] Gregor Richards, Sylvain Lebresne, Brian Burg, Jan Vitek. 《JavaScript程序动态行为分析》。 收录于第31届ACM SIGPLAN编程语言设计与实现会议论文集(PLDI ’10),第1–12页。ACM出版社,2010年。
  • [31] Marija Selakovic 与 Michael Pradel. 《JavaScript性能问题与优化:实证研究》。 载于第38届国际软件工程会议论文集,ICSE ’16,第61–72页。ACM,2016年。
  • [32] 菅沼俊夫、安江俊明、川人元博、小松英昭与中谷俊夫。Java即时编译器的动态优化框架。 载于第16届ACM SIGPLAN面向对象编程、系统、语言与应用会议论文集,OOPSLA ’01,第180–195页。ACM,2001年。
  • [33] Luke Wagner. Firefox Nightly中的asm.js | Luke Wagner的博客。 https://blog.mozilla.org/luke/2013/03/21/asm-js-in-firefox-nightly/。[在线;访问日期:2019年5月21日]
  • [34] Luke Wagner. WebAssembly里程碑:多浏览器实验性支持。 https://hacks.mozilla.org/2016/03/a-webassembly-milestone/,2016年。[在线;访问于2019年1月5日]
  • [35] 康拉德·瓦特. WebAssembly规范的机械化与验证. 收录于第七届ACM SIGPLAN国际认证程序与证明会议论文集(CPP 2018),第53–65页. ACM出版社,2018年.
  • [36] Christian Wimmer 与 Michael Franz. 基于SSA形式的线性扫描寄存器分配. 收录于第八届IEEE/ACM国际代码生成与优化研讨会论文集(CGO '10),第170–179页. ACM, 2010.
  • [37] Bennet Yee, David Sehr, Greg Dardyk, Brad Chen, Robert Muth, Tavis Ormandy, Shiki Okasaka, Neha Narula, Nicholas Fullagar. 原生客户端:可移植的x86原生代码沙箱. 收录于IEEE安全与隐私研讨会(Oakland'09),IEEE出版社,2009年。
  • [38] 阿隆·扎凯. asm.js. http://asmjs.org/. [在线; 访问日期:2019年1月5日].
  • [39] 阿隆·扎凯. Emscripten:LLVM到JavaScript编译器. 收录于ACM国际会议伴侣论文集:面向对象编程系统语言与应用伴侣,OOPSLA ’11,第301–312页. ACM出版社,2011年.

本文文字及图片出自 Not So Fast: Analyzing the Performance of WebAssembly vs. Native Code

共有 42 条讨论

  1. 考虑到他们使用自研的wasm内核模拟unix内核,从而能在浏览器中运行未经修改的unix程序,45%的性能下降幅度其实相当不错。他们能实现这一点本身就相当了不起,更令人惊叹的是它不仅能正常运行,而且如其他评论者所言,性能下降甚至未达数量级。

    我更关注两点:1) 浏览器中不涉及运行未修改Unix程序的Wasm应用场景;2) 浏览器外的Wasm应用——实现沙箱化/安全保障的“编译一次,随处运行”方案。这会是原生应用开发的未来吗?

    Kotlin、C#、Rust以及C/C++等语言对Wasm支持相当完善。若性能差距能缩小至10%左右,未来是否可能成为应用程序的合法目标?个人而言,比起原始二进制文件,我更倾向于运行具备(尽可能完善的)沙箱隔离机制的wasm二进制文件。

    编辑:该内容源自2019年,此后wasm已取得显著改进。

    1. > 浏览器外的wasm:实现沙箱化/安全保障的“编译一次,随处运行”场景

      我在DecentAuth[0]中正是这样使用的。效果超赞。只需将单一代码库编译为wasm,就能在JS、Go或Rust中调用该库。新增宿主语言仅需约1000行胶水代码,完全无需担心不同架构的构建问题。

      [0]: https://github.com/lastlogin-net/DecentAuth

    2. 浏览器外的wasm:实现沙箱化/安全保障的“编译一次,随处运行”场景

      请直接在微虚拟机中使用Docker。速度零损耗,成熟度百分百提升。

      1. > 请直接在微虚拟机中使用 Docker 即可。性能零损耗,成熟度百分百提升。

        Wasm 与 Docker 容器特性迥异,因而能针对不同场景和需求。例如:设想需要为游戏模组或演员系统部署数百甚至数千个插件,要求低延迟启动、低内存占用和低开销。这正是Wasm能胜任而容器难以实现的场景。容器虽适用于多数场景,但并非万能,Wasm仍有其独特价值。

        1. 确实,我主要看到它与Lua及安全沙箱中的小型函数执行(如eBPF的类似作用域)形成竞争。或许还能用于锁定那些对性能要求不高但存在问题的组件,比如各类驱动程序。

          同意,插件场景很合适。无论是游戏还是内核领域。

      2. 但难度大得多,攻击面也更广。

        而且两者未必完全可比。理想状态是能将编译好的WASM模块直接嵌入代码库,通过后端任意语言调用。这样就能跨服务复用大量代码,避免启动额外容器的开销。甚至可能以沙箱方式运行不可信代码。

      3. 这是否意味着我能在Windows和macOS上进行GPU运算和实时音频处理?

      4. 让终端用户配置并运行Docker来启动应用,对大多数场景而言根本行不通。

      5. 配置 Docker 和微虚拟机比直接用浏览器操作难上好几个数量级,体验也差得多。这两者完全不可互换。

  2. 居然连慢一个数量级都不到,这表现其实相当不错!

  3. 单一二进制文件运行所有环境慢45%…

    这笔交易我随时奉陪!

    1. 单一二进制文件运行所有环境慢45%…但安全漏洞更少,无未定义行为,且完全沙箱化轻而易举。

      绝对划算!

      1. > 没有未定义行为

        未定义行为是针对源语言定义的,而非执行引擎。这意味着语言规范未为某些源程序赋予明确含义。机器码(通常)不存在未定义行为,而C程序可能存在,无论运行在何种平台上。

      2. 原生代码通常不存在未定义行为。C语言本身存在未定义行为,这才是核心问题——无论编译为原生代码还是WebAssembly都无法规避。

    2. 旧事重提,万物轮回…

      等等,我们可以用Java在任何地方运行?虽然慢点无所谓!走起!

      1. Java小程序在所有浏览器中被弃用是有原因的。其运行时环境天生不安全,根本不适合网络应用。

        此外,针对JVM开发意味着必须接受垃圾回收机制、基于类的面向对象编程以及大量指针追踪操作。这对大多数语言而言并非理想目标。

        Java本身相当优秀,但WebAssembly才是真正的变革者。

        1. Java运行时与JavaScript运行时在安全性上并无本质差异,而JavaScript在网页环境中运行得相当顺畅。

          Applet安全机制失败的关键在于默认赋予用户完整JDK权限,导致每个JDK方法都需添加显式安全检查代码来限制访问。这种默认全权控制、选择性禁用的模型本末倒置——JDK的每个新特性都可能成为新漏洞。

        2. 我是Wasm的超级粉丝。初次将Qt应用编译为Linux、Windows、Mac和Wasm目标平台时,兴奋得几乎要跳起来。那感觉就像真正站在巨人的肩膀上,让我深刻体会到整个技术栈的价值。

          在浏览器中运行代码并非新鲜事,这其实是个循环论证。前几天我还遇到个认为JavaScript是Java子集的人,此人居然还精通PHP。

          Wasm确实很棒,我真心喜爱它。但我的怀疑论观点是:归根结底,它终将助推广告收入开辟新的利润空间。

          1. 有道理。浏览器运行虽非首创,但JS/TS能成为史上最流行语言,若非垄断浏览器绝无可能。

            利润空间扩大我无异议,但反竞争市场不可取。我期待Wasm能打破某些平台的垄断枷锁(咳咳 iOS 咳咳 Android)

            1. 我实在不认为苹果会放任任何人过度将iOS浏览器应用化。

  4. 慢45%意味着…?

    假设原生代码执行耗时2单位时间。

    “慢45%”是???

    难道是多耗费45%的时间?

    那么“快45%”又该如何理解?

    1. 相关表格的摘要行标注着“几何平均值:1.45倍”,因此我认为此处“慢45%”即表示“耗时延长至1.45倍”。

      (我通常会用“x%更慢”表示“慢了1+x/100倍”,用“x%更快”表示“快了1+x/100倍”,因此“x%更慢”和“x%更快”并非相反概念——完全可能存在300%更快或300%更慢的情况。我 不太确定 多数人是否这样使用这类表述。)

    2. 这个观点很合理,这种表达方式确实容易混淆。是指原始时间增加45%?还是原始速度降低45%?

      我认为用吞吐量来理解更直观:

      即单位时间内完成的工作量减少45%,相当于完成55%的工作量。

    3. 0% 慢意味着“相同速度”,即耗时相同秒数。

      10% 慢意味着“耗时增加10%”,即多耗费10%秒数。

      因此相较于2秒,45%的延迟即为 1.45 × 2 = 2.9 秒。

    4. 若表述为“原生应用仅耗费WASM对应方案的x%资源”或许更清晰。

  5. 这些数据很有意思,但需注意其源自2019年,此后技术已大幅进步。

  6. 考虑到剩余的优化空间有限,且替代方案JS通常慢2-10倍,这个表现其实相当不错。

    据我所知,向量化支持将缩小整体性能差距——许多SPEC测试本就受益于自动向量化。

  7. 确实,我在测试Rust代码编译为原生和WebAssembly时也观察到这种现象。不过45%这个数字我不确定,尚未实际测量过。

  8. …在浏览器中。浏览器最多只能进行即时编译。而某些WebAssembly运行时支持预编译,性能显著提升(例如仅慢5-10%)。

    标题极具误导性。

    1. 网页浏览器 中测量 Web Assembly的性能并不算误导。

      1. WebAssembly既非网页技术也非汇编语言,它是一种低级别的字节码格式,最接近LLVM中间表示(IR)。

      2. 是的,但它专门测试的是那些基于posix API实现的组件(因为通常所谓的“原生”API都是这样做的) (此处省略运行时加载的libc及其他操作系统基础库)我认为若应用程序链接到类似WASI的运行时环境,或许能提供更优的评估标准(原生WASI作为库文件与需额外链接的WASI运行时对比)。不过请注意这仍无法解决浏览器运行时的问题…但对WASM与原生性能的对比会更具参考价值。

        但正如先前所述,这些讨论我们早已经历过。或许我们将见证Wasm字节码像JVM那样通过硅片加速——尽管这次技术可能真正落地,或迁移至服务器硬件(虽然嵌入式设备支持硬件级JVM字节码的情况可能已存在)。

        简言之,标题中省略了浏览器相关部分。

    2. 这意味着浏览器终将迎头赶上。

      初始运行较慢,但完全编译后反而更快

    3. 自WebAssembly诞生起,浏览器就已实现(有时分层的)AOT编译。

      1. WAMR(WebAssembly微运行时)、WABT(WebAssembly二进制工具包)中的wasm2c、Wasmtime。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

你也许感兴趣的: