浅谈WebAssembly
WebAssembly作为新的前端标准之一,在前端有着举足轻重的地位,它到底是一种什么技术,它的过去,未来到底是什么样子,在这里做一些总结和介绍,主要介绍思路和相关示例来自于WebAssembly for Web Developers (Google I/O ’19)和Rust, WebAssembly, and the future of Serverless by Steve Klabnik
Web前端发展史
要了解WebAssembly,就不得不从前端的发展史来讲起。网络的发展是非常漫长的,最开始的时候,我们有了HTML(Tim Berners-Lee和同事Daniel W. Connolly创立于1990 ),可以显示一些静态的文档。但是我们并不满意,所以CSS(Håkon Wium Lie创立于1994)出现了,我们可以给网页添加一些样式。接着,我们想要用一些动作来控制网页,所以JavaScript出现了。
graph LR
HTML-->CSS-->JavaScript
JavaScript
说到这里,我们不得不谈论一下JavaScript。
Web从来都是野心勃勃的,从纯文本到JavaScript,给了我们无尽的幻想,其发展大大扩展了网络作为一个供我们交互的平台的功能。我们打开电脑,打开浏览器,通过浏览器提供的Web应用和大家进行交流,浏览器就像一个小型的操作系统。一直以来,开发者们穷尽自己的想象力,想把网络推向极限,让我们可以在浏览器上面做更多想做的事情
从另一个角度想,实际上,我们现在有那么多的手机端的APP,和各种桌面应用程序,也是因为想象力不够,或者妥协的结果。试想一下,如果所有现有的APP,桌面软件都可以通过Web的形式使用,是否才是网络的最终形态呢,互联网就好像一个USB一样,随时和我们的电脑连接,不需要各种繁琐的安装,注册表,等等一切
那么,是什么阻挡了我们的想象力呢,是不思进取的IE
,还是一心搞应用商店的苹果?这些都可以算得上是因素之一,但是从技术上来说主要原因,还是性能
Javascript ,是1995年Brendan Eich用10天的时间设计出来的,设计这个语言当时只是公司的任务,他也没想到这个语言未来会成为互联网最流行的语言,不然可能会更用心一点设计?
Javascript为什么会成功
- 碰瓷(误);
- 设计理念很简单,就是易学易用
Javascript的缺点
解释型语言,非常慢,随着前端的发展,网页应用越来越复杂,解释型语言在性能方面存在劣势。
V8和asm.js
随着Google在2008引入了V8引擎,才有了JS现在JIT(Just-in-time)的概念,思想就是,解释型语言,如果要运行多次,肯定就有优化的空间,于是V8就偷偷帮你编译好,在下一次运行的时候,直接执行编译好的代码。但是由于JS动态类型的特点,很多时候反而会造成负优化(JS被编译成错误的字节码,不得不重新编译的情况 )
2013年,一个来自Mozilla
,也就是那个开发火狐浏览器的公司,发布了asm.js
项目,这也是很有野心,也相当成功的尝试之一
asm.js是一个中间语言规范,设计目的是使采用C等编程语言编写的计算机软件可运行为网络应用程序,同时性能特征明显优于标准JavaScript。
asm.js通常不直接编写,而是作为一种通过编译器生成的中间语言,该编译器获取C++或其他语言的源代码,然后输出asm.js。
例如,提供下列C语言代码:
1
2
3
int f(int i) {
return i + 1;
}
Emscripten将输出下列JavaScript代码:
1
2
3
4
function f(i) {
i = i|0;
return (i + 1)|0;
}
对人类来说,这些或操作可能是无操作,但是对于编译器来说,这些按位运算符可以说明操作数的类型,并给出整数结果。经过这些转换之后,优化编译器才能提前生成高效的本地代码,保证定义的是32位整数,而不是浮点数(其实不难发现,现在很多人将不同的语言:jsx
,ts
等编译成js
,以此来解决在使用原生js
开发的时候可能遇到的问题,js
有成为前端汇编语言的趋势,这说法可能有些滑稽,但事实正是在这样发展)。
可以看得出,这样的写法很hack,有点像在滥用语义获取整数,而且事实上,只有一小部分代码适用这种优化,但即便如此,js
的性能也得到了大幅提升。由于asm.js
的诞生,我们有了诸如https://blog.mozilla.org/blog/2014/03/12/mozilla-and-epic-preview-unreal-engine-4-running-in-firefox/z这样的成果,虽然画质在今天看来不算特别好,但仍然是非常了不起的成就,asm.js
大幅提高了js
的运算能力(画面是通过WebGL来实现的,这是一套JavaScript API,使用它不需要任何插件就能在任何兼容的浏览器上渲染交互式的3D和2D图形,其是OpenGL的一个子集)
此时用到的编译器是Emscipten,也是Mozilla做的,当需要一些只能在非Web环境上调用的内容时,比如前面提到的OpenGL,或者fileopen功能等,Emscipten可以用WebGL伪装成OpenGL,也可以通过模拟一个文件系统,使你好像在处理真实的文件(意思是在编译的时候额外编译一些需要使用的文件进去,让执行代码的时候可以读取它们,并不代表可以随意打开本地文件),几乎相当于是真的模拟了一套POSIX操作系统来让代码运行在Web上 ,而这些代码最开始不是为了Web编写的
WebAssemlby
所以这次,发展方向改变了,为什么要大费周章用C/C++编译成js,再用JIT去翻译,直接给字节码不好吗?
终于有一次,四大浏览器厂商,谷歌、微软、苹果和Mozilla一致决定,不能再这样下去了,从立项开始,花费几年的时间,在2019年的12月5日,W3C宣布,继 HTML、CSS 和 JavaScript 之后,WebAssembly成为 Web 的第四语言,也就是我们今天要谈论的主角
当你使用任何语言编写代码并将其编译到WebAssembly时,这些代码会被编译成浏览器可以执行的指令集,然后这些指令集以二进制格式存储到.wasm
文件中
想象一下,像AutoCAD
这样的软件,正在移植到WebAssembly,当你想用它时,可以理解在浏览器上运行它,多么不可思议,另外Unity游戏引擎,还有虚幻引擎,都开始支持WebAssembly,通常这些游戏引擎已经内建了一套抽象,用于编译到PlayStation,XBox或其他游戏平台,现在WebAssembly就可以直接使用这些代码,并且提供所需要的性能,这是在asm.js
时期不敢想的
还有一个例子,我们做了一个网页,在网页中让用户扫描二维码,并且识别二维码中的内容,识别二维码这个功能(图形检测API)是有些浏览器内置的,当遇到你的浏览器不支持的时候,他们就会把二维码库编译到WebAssembly上,就可以按需加载,实现这一功能,可以看到,通过WebAssembly,我们获得了浏览器本身没有的功能
另外还有UI工具包QT宣布,他们现在也支持WebAssembly了https://www.qt.io/qt-examples-for-webassembly
当使用C++时,用到的编译器是仍然还是Emscripten,它替代C/C++编译器,不将代码编译成本地机器码,而是直接编译到WebAssembly,大家可能还记得我们在前文提到过它,将C/C++编译成符合asm.js
标准的JavaScript代码,这次,Emscripten不用编译成JavaScript,而是直接编译成WebAssembly,此时,有了这一套完整的工具链,我们就能围绕WebAssembly做更多成熟并且令人印象深刻的工具,这也是为什么WebAssembly与C++如此紧密的缘故
可能你会说,我就是一个普通的前端开发人员,前端生态已经够让人头大了,我必须要学习C++才能用这个对吗,答案是,No!因为很明显,这两种语言的差别还是挺大的,不从语言的好坏难易来评价,就从编程习惯和思想来说,相差也不是一点。而且即使这两种语言都写得很专业的人,在开发过程中不断切换也是会花费一些成本的,对于Web开发人员来说,更加没有去学习C++的动力了。这样一想,WebAssembly似乎是一个非常小众的技术
WebAssembly到底拿来做什么
这里我们从两个方面来讨论这个疑惑,首先我们讨论JS应用中微小模块的替换
JS生态非常庞大,近些年甚至由于技术栈和一些愿意尝鲜的人的原因,已经出现了很多JS开发的桌面应用,但很明显,不是每个主题和功能的首选都是JavaScript,所以很多时候你可能面临一个问题,你需要库去解决这些问题,你可以在C或者Rust中找到这些答案,但不是在JavaScript中。因此,你要么坐下来编写自己的JavaScript实现,要么通过WebAssembly来使用其他语言的相关实现,比如SQuoosh,这是一个完全在浏览器中运行的图像压缩应用程序,可以在脱机状态下工作,放入图片,然后用不同的编解码器压缩它们,可以从这里观察到不同的编解码器对图片质量产生不同的影响。如果比较了解的话,可以知道现代浏览器已经可以通过Canvas
实现这个功能了,但事实证明,实现的效果不尽人意,而且,会受浏览器支持的编解码器的约束,所以一直到很长一段时间,也只有Chrome可以编码为WebP格式,其他浏览器却没有。
我们用浏览器搜索,找到了一些使用JS有关的JPEG的编解码器,但是却没有找到专门针对WebP的编码器,所以我们从其他方向查阅了一番,发现C/C++有大量相关编码器,因此,我们选择WebAssembly,将相应的库编译到WebAssembly上,并加载到浏览器中,然后使用我们自己加载的这个编解码器来处理图片,从而完成图像压缩。并且,我们的库提供了更专业或者说更细致化的功能定制,这些显然是浏览器自带的功能不会暴露给我们的。这整个过程的重点是,我们使用了一段旧代码,它绝对不是以Web为目的写的,但是我们还是将它使用在了Web上,并用来改善Web平台
具体我们是怎么做的呢,首先我们使用Emscripten来编译这个库,以便之后可以链接到它(注意,图片编解码通常是一个高并发任务,但是由于不论是JS或是WebAssembly都还不支持多线程,所以我们禁用它), 编译完成后,除了.wasm
文件之外,我们还会得到一个.js
桥接代码,用来调用编译好的.wasm
文件,这所有的步骤,编译器都会帮你完成
另外值得一提的是,这个应用的Resize功能使用的图像缩放算法,实际上是来自于Rust生态,对于Rust程序员来说,可以使用wasm-pack
将Rust代码转化为WebAssembly模块,并且生成的文件非常小,在这里比较大小有点有失公平,因为本来它们都是两个不一样的库,但是就平均而言,Rust通常会产生更小的文件,因为Rust没有做任何关于POSIX的文件系统模拟,所以不能在Rust代码中使用文件相关操作(如果想要的话,也可以使用封装好的模块,所以这个功能是可配置的,比较灵活 )
从这一整个工具中我们可以看到,我们使用了至少来自两种不同语言的四个不同的库,即使这它们跟Web无关,我们还是在Web上使用了它们,并且工作得很好。所以你看,如果我们在Web平台上发现一个技术上的空白,但是在其它语言已经实现了很多次,你应该想到WebAssembly是一个可能的工具。所以WebAssembly出现的目的,至少到目前为止的目的,不是为了取代JS,而是为了和JS配合,完整Web生态
接着让我们关注性能,首先,JavaScript和WebAssembly都有相同的峰值性能,但是在运行已经编译好的比较稳定的代码这点上,使用WebAssembly比Javascript快得多,因为JavaScript是弱类型的,在编译过程中容易出现一些意外情况,是编译器无法捕捉的,并且WebAssembly技术团队正在寻找多线程和SIMD技术的解决方案,由于语言的限制,这些是JavaScript永远无法做到的
为了体会什么叫”运行已经编译好的比较稳定的代码”这一点,我们来比较两者的入口
Ignition是一个解释器
虽然我们不能以偏概全的说机器码永远比解释型语言代码跑得更快,但就大多数情况而言,确实是这样,对于js而言,优化编译器总是只会在最后启动,也就是说只有当JS代码运行的时候才能进行优化,假设编译出来的代码没有包含所有情况,就必须再回到解释器,这被我们称为负优化,有了WebAssembly之后,我们的步骤永远都是从编译器到Turbofan
当然这边有一些来自网络的图表数据,来证明相同情况下它们速度的对比,但是我们重点想要说的是,因为js的动态类型特性,导致它速度的不稳定性,而且对于不同的浏览器,其对js的优化可能也不完全相同,在不同浏览器中的表现可能差距很大,而WebAssembly总是能带来我们想要的稳定性,这才是最重要的,这也是我们想说的,最优性能
另外,如果前端想要编写WebAssembly程序的话,他们也可以用AssemblyScript,它是完全基于TypeScript语法的,这意味着现有的前端人员也完全不需要学习一门全新的语言来使用WebAssembly
他们并不是竞争对手,但是不存在谁取代谁的关系,他们应该协作使用,至少目前是如此
Rust
带点私心的,让我们谈论一下Rust
Rust:A language empowering everyone to build reliable and efficient software.
https://baike.baidu.com/item/Rust%E8%AF%AD%E8%A8%80/9502634?fr=aladdin
Rust是一种系统级的编程语言,其最早是Mozilla在开发Firefox时用来作为C++的替代品,意在减少语言带来的安全漏洞(大部分来自于内存安全方面,由于错误使用指针)
以往在考虑编程语言时,往往要在效率Speed和Safety之间做抉择,以往的编程语言曾经在速度上非常惊人,并且根本不关心安全性,这是因为以前计算机运行缓慢,我们要尽力用完每一部分可以利用的资源
微软的CVE(Common Vulnerabilities & Exposures通用漏洞披露),深蓝色是内存安全相关问题,浅蓝色是所有其它问题,当然这不是针对C/C++,很多高级语言延伸到低级语言以提高速度,所以你还是有很大概率会面对内存安全的问题
拿汇编语言来说,汇编太危险了,所以创造了C,其拥有更高的级别,虽然会获得更少的效率上的收益,但是可以增加安全性,让开发者更难编写不正确的程序
GC机制让运行时自动为我们完成内存分配和释放相关的工作,但是问题也随之而来,它会让语言的效率降低,所以我们需要在速度和安全性之间进行权衡
我们可以选择像Ruby这样很容易安全使用但是效率低下的语言,或者选择相对效率高,但是需要认为考虑很多情况,相对危险的C/C++
我们能不能同时拥有两者呢,对于Rust来说,Yes
对于Rust来说,权衡的结果是学习起来会更加困难,并且你可能会遇到许多编译器错误,因为编译器非常严格
从垃圾收集器,我们得到了一个概念,运行时(浅灰色代表我们自己的代码,深灰色可以看成是运行时代码),这意味着在程序运行的时候,语言会把实际上没有编写的程序放进程序中,当使用JavaScript的时候,我们用的是V8引擎
Rust 有运行时吗?
没有像 Java 语言这样典型意义上的运行时。但 Rust 标准库的一部分可以被认为是一个「运行时」,它提供了堆(heap),回溯(backtraces),展开(unwinding)和栈保护(stack guards)。在用户的
main
函数之前只运行很少的初始化代码。Rust 标准库还链接到 C 标准库,执行类似的运行时初始化。 Rust 代码可以在没有标准库的情况下编译,在这种情况下,运行时大概相当于 C。
所以很有趣的一点就是,Rust的问题,很多都是编译时候的问题,即使从来没有用过C/C++的人,想编写底层软件,也不会造成那么可怕的后果,所以不难明白,为什么Rust和WebAssembly有着密切的关系了吧。Rust真的非常优秀,作为一名喜欢Rust的程序员,非常希望更多人也来感受Rust带来的安全性和效率
WebAssembly的未来
随着浏览器的发展,还有WebAssembly生态以及特性(多线程,操作DOM)等不断健壮,我们可以大胆猜测在不久的未来,它会在Web领域占据举足轻重的地位。甚至可以试想,在带宽和电脑性能不断增强的今天,是否以后我们可以不用下载任何其他应用软件,就在浏览器里使用各种WebAssembly编写的工具,甚至直接玩画质更加优秀的游戏呢?这值得期待
结尾可能有点草率,哈哈哈,未来趋势这个东西谁也说不好,一个技术未来会不会火也不全取决于它好或者不好,创新性,不可替代性,易用性,所在的生态圈等非常多的因素都会对它未来的发展造成影响。但还是想说,我看好WebAssembly的未来