WebAssembly

产生背景
WebAssembly官网上,提出一种观点:传统的网络架构可以分为两层,一层是运行web app的虚拟机,一层是web API。可以理解为虚拟机层用于计算,实现交互功能,而web API层负责实现展示



JS性能瓶颈
一直以来,虚拟机层只需要加载js就够了。因为js的功能足够强大,而且随着这20多年的发展历史,js自身也得到了很充分的扩展,形成了完善的生态系统。但是,功能提升的同时,js也慢慢变得臃肿了起来。并且,随着web应用范围的逐渐扩大,js逐渐显得力不从心
比如,我们在google上搜索“js 性能“,有将近两千万条结果。前端面试中也常见到性能优化问题。这充分说明了js性能问题始终是前端方向的热点问题。
虽然现在有了node和高效的v8引擎,但是在计算复杂性任务上,js性能还是远远落后于传统编译型语言



Web应用范围扩大
如今,web的应用范围已经不再是简单的资源展示和提供了。近些年来,基于web的VR,AR技术,3D游戏等都有蓬勃的发展,例如我们818活动“发烧星球”玩法,就是一个web VR的实用例子
虽然也有类似WebGL这样的高性能框架出现,通过连接OpenGL的底层接口,为原本极度十分耗时的h5 canvas渲染提供3D硬件加速,大大提高了js在图像处理上的能力。但是我们日常面对的很多业务,由于面向用户范围很大,必须要考虑到兼容性的问题。在很多场景上我们并不能使用新技术,只能用canvas来保证兼容。从这个角度看来,js依旧存在着很大的劣势
总结一下,js的问题主要来源于以下两点:



  • js作为一门动态语言,缺少编译过程,直接由浏览器解释执行。虽然对于开发者十分友好,但与此同时,js运行时需要大量的类型推导,也就是说需要在运行时动态决定的计算量太大(需要做大量分支预测),造成其效率远低于传统编译型语言。这也就是所谓的“牺牲了部分性能,换取了更强的表现力”

  • web应用范围的扩大使得js必须面对更多更复杂的场景。这远远不是js设计时所预想到的(毕竟第一版js只用了10天就做出来了……)

    WebAssembly - 入门
    WebAssembly是一种小体积,高加载速度的二进制编码格式
    从名字就能知道,这是一门底层汇编级的语言。有了WebAssembly,我们的虚拟机层就将会同时加载和运行两种类型的代码——JavaScript和WebAssembly。
    这两种代码可以通过WebAssembly所提供的js api实现互相调用。事实上,WebAssembly代码的基本单元被称作一个模块,并且这个模块在很多方面都和ES2015的模块是等价的。所以我们可以认为WebAssembly模块是一个“高性能的JS函数”
    WebAssembly不是用来取代JavaScript的。它被设计为和JavaScript一起协同工作,从而使得网络开发者能够利用两种语言的优势
    WebAssembly设计的目的不是为了手写汇编级别代码,而是为诸如C、C++等低级源语言提供一个高效的编译目标,使得以各种语言编写的代码都可以以接近原生的速度在web中运行。这一点具有重大的意义,这意味着所有由传统语言编写的客户端app都可以在web上高效运行,也就是说在未来客户端全面web化,未来可能不再需要客户端app
    同时,WebAssembly也是一个W3C标准,制定过程中得到了各大浏览器厂商积极参与。各大厂商都参与到标准制定里并不常见,像js引擎,css标准,每个浏览器实际都有一套自己的标准。而获得各个厂商支持的WebAssembly在我看来,是未来的标准风向,会被广泛采用



WebAssembly优势
高效,跨平台
WebAssembly是二进制的,可以直接在WebAssembly虚拟机上的机器代码(可以类比于jvm 字节码)
类比于汇编,如果机器的指令集和架构相同,则机器码可以直接执行,不关心上层os环境。Webassembly也一样能在不同平台上获得高效执行
沙箱化执行环境
WebAssembly被限制运行在一个虚拟的的沙箱执行环境中,运行时产生的变化可以随后删除,不会对系统产生永久性影响。并且它严格遵循浏览器的同源策略和授权策略,进一步确保了安全性
有文本格式,可读可调试
类比于汇编。每一条指令有对应的二进制值
无版本,标准化
WebAssembly是无版本,向后兼容的。这一点很有意义,相信大家在开发中也很经常碰到版本带来的一些很坑爹的问题。由于系统体积很大,依赖繁杂,且高级依赖往往不兼容低版本,造成升级时的巨大困难
其次,WebAssembly无论是在PC端还是移动端,都支持各种浏览器平台
几个常见概念
Module
一个“代码单元”。包含编译好的二进制代码。可以高效的缓存、共享
未来可以像一个ES2015模块一样导入/导出
Memory
连续的,可变大小的字节数组缓冲区。可以理解为一个“堆”
Table
连续的,可变大小的类型数组缓冲区
现在table只支持函数引用类型,可以类比为一个“栈”
Instance
在Module基础上,包含所有运行时所需状态的实例
如果把Module类比为一个cpp文件,那么Instance就是链接了dll的exe文件
构建方法
直接汇编文本编写
WebAssembly使用S-表达式作为文本格式
S表达式用于表示一棵树。树上的每个一个节点都有一对小括号包围。括号内的第一个标签告诉你该节点的类型,其后跟随的是由空格分隔的属性或孩子节点列表
劣势显而易见,编码逻辑不容易理解



移植一个C/C++程序



这张图是官网上的构建流程图,构建过程中使用了Emscripten——一个基于llvm的编译器,目的是把c/c++编译为asm.js(js的一个真子集)
我们知道,c和js语法十分相似。所以在c到js的编译过程中,要解决的最重要的问题主要是两点:



C/C++是静态类型,js是动态类型
C/C++需要程序员手动管理内存,js则有自己的一套垃圾回收机制
因此,就出现了asm.js。asm.js只有两种静态类型(i32, f64),并取消js的垃圾回收(手动管理内存)。浏览器加载到asmjs时,不进行语法分析,直接翻译为机器码执行
实际上,asm.js就是WebAssembly的一种文本格式,但不同于之前提到的s表达式。这一点类比于c,汇编语言,机器码之间的关系
由于WebAssembly当前不能直接调用Web API(如存取DOM),它只能调用JavaScript,因此需要一段js胶水代码使WebAssembly能够调用到Web API
移植代码缺点在于需要较复杂的依赖,相比之下,汇编编写依赖都由程序员自己定义



使用方法
WebAssembly的模块在很大程度上和ES2015模块类似。在使用上也是分为两步:加载和调用



加载
获取.wasm二进制模块文件
编译为Module
实例化为Instance
由于获取,编译和实例化都是异步的,所以实际使用中为了方便,可以直接构建一个异步的loader对wasm进行加载
调用:从Instance中获取函数接口
性能对比
使用斐波那契数列作为计算函数
(C代码如下)



void fibonacci(int n)
{
int first = 0, second = 1, next;
for (int i = 0; i < n; i++)
{
next = first + second;
first = second;
second = next;
}
}



重复计算一百万次斐波那契数列46项(47项会溢出),结果如下:



  • C:3ms

  • JS: 70ms

  • WebAssembly:11ms
    可以看出,WebAssembly在计算复杂型任务中效率远胜过原生JS



https://cloud.tencent.com/developer/section/1192270



WebAssembly是什么?
WebAssembly 是一种能把除了JavaScript以外的编程语言编写的代码经过编译器编译转换为能在现代浏览器中运行的代码的技术。众所周知,JavaScript在 Web 中的地位一直独步天下,无 yan 能敌,所以WebAssembly所指的性能优势,是针对 JavaScript 而言的。WebAssembly并不是为了替代 JavaScript 出现的,而是希望与 JavaScript 并驾齐驱共同开发出性能更高的应用。



首先看看 JavaScript 的性能历史
JavaScript诞生于1995年,目的是为了给浏览器 HTML 网页增加动态交互功能,并没有考虑太多性能问题,事实证明在前十年里浏览器也不需要它有多快。一切变化发生在2005年,谷歌在多款交互应用中使用Ajax技术让交互体验得到了极大的提升,让人们认识到了原来网页能做的事情远远不是内容的展示和表单的提交。



到2008年,JavaScript在浏览器中低下的执行效率,已经成为限制程序猿们在网页施展拳脚的一大阻碍。突然,有个叫谷歌的厂商推出了一款叫Chrome的浏览器,它与以往浏览器最大的不同在于内置了一个 JITs(just-in-time compilers),一个能在 js 代码执行时根据某些模式动态编译代码为能在浏览器中更高效执行的代码的技术,详细介绍可以看这篇文章: a crash course in just in time(jit) compilers。从此拉开了浏览器性能大战的序幕。



时间再推进10年到2018年,此时JavaScript的使用场景已经远远超过了原先的想象:服务端、网页游戏、WebVR/AR、图片/视频处理等等场景,JITs 都已经不能满足这些对性能日益严苛的场景了,此时我们需要更进一步的突破,这个就是 WebAssembly。



为什么WebAssembly更快?
就如第一部分所提,WebAssembly的性能优势是针对 JavaScript 而言的,下面我们分别从 JavaScript 和 WebAssembly 的执行过程一一对比优势到底在哪:



获取
由于 WebAssembly 是由编译器生产出来的,并且将被浏览器直接解析执行,可以节省那些为了给人类阅读而添加的不必要代码,从而可以做到文件大小甚至比经过压缩的 JS 代码更小。所以在相同的网络情况下,从服务器获取一个 WB 文件会比获取一个 JS 文件 更快。



解析
当 JS 文件成功到达浏览器之后,浏览器会将其解析成一棵抽象语法树(Abstract Syntax Tree)(但是只会先解析当前需要执行的那部分代码,而其余未执行的函数将会保存成存根),然后再转换为 JS 引擎识别的 IR(intermediate representation) 层字节码(认识 JAVA 的应该对这个词不陌生)。



反过来我们看 WebAssembly 本身已经是经过高级语言编译出来的 IR 层代码了,不需要在浏览器端进行解析而只需要把经过压缩的内容解码出来,节省了相当多的时间。



编译和优化
这个阶段是 JITs 负责做的事情,不同浏览器对 WebAssembly 的处理可能会有细微差别,我们以都使用 JITs 进行优化的场景来看看为何 WebAssembly 会比 JavaScript 更快,有以下三点:
(阅读下面内容需要先对 JIT 有一定认识,不清楚的可以先看 这里



由于 WebAssembly 的输入类型是固定的(byte),所以不需要通过运行代码这种方式去检查输入类型来进行编译优化;
在 JavaScript 中相同一段代码可能因为输入值不同需要分别编译成不同的版本,而 WebAssembly 也不需要进行这种冗余的操作,原因如上;
3、 WebAssembly 在从高级语言(C/C++/Rust)编译而来的时候,已经经过编译器优化一次了,所以在 JITs 中需要做的事情更少;
重优化
还是由于 JavaScript 动态类型的原因,一段经过了深度优化的代码,可能因为这次执行的时候输入值类型变了,导致 JITs 需要根据输入值类型重新进行一次上一步的优化工作,这也需要花费一定的时间。



而 WebAssembly 输入值固定,JITs 不需要在每次代码执行时去计算输入值的类型,从而不会发生重优化这样的事情。



执行
JavaScript 代码一般是人写的,而 WebAssembly 是由编译器编译出来的,是直接针对机器产生的代码,会包含更多对机器性能优异的指令(instructions),这部分差异针对不同的功能代码 WebAssembly 可能会比 JavaScript 快 10%~800%。



垃圾回收
我们都知道在 JavaScript 中不必人工去执行变量的释放和内存的回收,因为 JS 引擎有自动垃圾回收功能,能自行判断该回收什么东西甚至足够智能知道在何时进行回收操作。但是这还是存在天花板可能会影响代码的执行。



在目前为止,WebAssembly 都不支持自动垃圾回收,内存由代码手动管理(由于使用了 C/C++编写),这将会加大开发者编码的难度,但能保证代码性能更可控。



总结
总的来说,大多数场景下 WebAssembly 比 JavaScript 性能更好是因为:



WebAssembly 代码更小的体积;
解码 WebAssembly 比解析转译 JavaScript 用的时间更少;
优化 WebAssembly 的用时比优化 JavaScript 的更短,因为前者是已经经过一次编译优化并且面向机器的代码;
WebAssembly 没有重优化这个过程;
WebAssembly 包含对机器更友好的指令;
JavaScript 无法人为控制垃圾回收,而 WebAssembly 可以有效控制内存回收的时机;
=============



本文主要知识和灵感来源于:



  1. A cartoon intro to WebAssembly系列文章,感谢作者。后续本博客会翻译几篇此作者编写的 WebAssembly 使用教程。

  2. WebAssembly Concepts

  3. WebAssembly format


Category golang