系列文章目录
文章目录
前言
WebAssembly作为一门新兴起的技术,大概是在2018年才由谷歌公司的程序员进行公布,在国内网站上关于此类的文章寥寥无几,但是如果翻墙一看,其实他已经在 JavaScript 圈非常的火!由WebAssembly编译出的wasm文件已经被 Firefox, Chrome, Safari, Edge 支持。
WebAssebmly带来的显而易见的好处是大幅度提高了浏览器的性能,由于博主不是前端工程,所以关于性能这一块我并不会过多的进行介绍,重点是普及一下不知道WebAssebmly的朋友,它是什么,有什么用户,以后会如何发展。
|版本声明:山河君,未经博主允许,禁止转载!
一、WebAssembly是什么?
WebAssembly是一种运行在现代网络浏览器中的新型代码,并且提供新的性能特性和效果。它设计的目的不是为了手写代码而是为诸如C、C++和Rust等低级源语言提供一个高效的编译目标。
对于网络平台而言,这具有巨大的意义——这为客户端app提供了一种在网络平台以接近本地速度的方式运行多种语言编写的代码的方式;在这之前,客户端app是不可能做到的。
而且,你在不知道如何编写WebAssembly代码的情况下就可以使用它。WebAssembly的模块可以被导入的到一个网络app(或Node.js)中,并且暴露出供JavaScript使用的WebAssembly函数。JavaScript框架不但可以使用WebAssembly获得巨大性能优势和新特性,而且还能使得各种功能保持对网络开发者的易用性。
二、WebAssembly的目的
WebAssembly提供了抓换机制(LLVM IR),把高级别的语言(C,C++和Rust)编译为WebAssembly,以便有机会在浏览器中运行。主要是解决目前JS语言的效率问题,设计立足点为快速,内存安全和开放。
但WebAssembly是一门不同于JavaScript的语言,但是,它不是用来取代JavaScript的。相反,它被设计为和JavaScript一起协同工作,从而使得网络开发者能够利用两种语言的优势:
JavaScript是一门高级语言。对于写网络应用程序而言,它足够灵活且富有表达力。它有许多优势——它是动态类型的,不需要编译环节以及一个巨大的能够提供强大框架、库和其他工具的生态系统。
WebAssembly是一门低级的类汇编语言。它有一种紧凑的二进制格式,使其能够以接近原生性能的速度运行(注意,WebAssembly有一个在将来支持使用了垃圾回收内存模型的语言的高级目标)。
所以它其实是一种运行机制,一种新的字节码格式(.wasm),而不是新的语言。
三、WebAssebmly的目标
WebAssembly是为下列目标而生的:
- 快速、高效、可移植——通过利用常见的硬件能力,WebAssembly代码在不同平台上能够以接近本地速度运行。
- 可读、可调试——WebAssembly是一门低阶语言,但是它有确实有一种人类可读的文本格式(其标准即将得到最终版本),这允许通过手工来写代码,看代码以及调试代码。
- 保持安全——WebAssembly被限制运行在一个安全的沙箱执行环境中。像其他网络代码一样,它遵循浏览器的同源策略和授权策略。
- 不破坏网络——WebAssembly的设计原则是与其他网络技术和谐共处并保持向后兼容。
四、WebAssebmly工作原理
1.WebAssebmly关键概念
为了理解WebAssembly如何在浏览器中运行,需要了解几个关键概念。所有这些概念都是一一映射到了WebAssembly的JavaScript API中。
-
模块:表示一个已经被浏览器编译为可执行机器码的WebAssembly二进制代码。一个模块是无状态的,并且像一个二进制大对象(Blob)一样能够被缓存到IndexedDB中或者在windows和workers之间进行共享(通过postMessage() (en-US)函数)。一个模块能够像一个ES2015的模块一样声明导入和导出。
-
内存:ArrayBuffer,大小可变。本质上是连续的字节数组,WebAssembly的低级内存存取指令可以对它进行读写操作。
-
表格:带类型数组,大小可变。表格中的项存储了不能作为原始字节存储在内存里的对象的引用(为了安全和可移植性的原因)。
-
实例:一个模块及其在运行时使用的所有状态,包括内存、表格和一系列导入值。一个实例就像一个已经被加载到一个拥有一组特定导入的特定的全局变量的ES2015模块。
2.wasm文件
使用WebAssebmly编译C++文件后,会生成.wasm文件,该文件其实是编译出的二进制文件,如果现在有一文件为
1 int add(int num1,int num2) {
2 return num + num2;
3 }
那编译出来的wasm文件为
1 00 61 73 6D 0D 00 00 00 01 86 80 80 80 00 01 60
2 01 7F 01 7F 03 82 80 80 80 00 01 00 04 84 80 80
3 80 00 01 70 00 00 05 83 80 80 80 00 01 00 01 06
4 81 80 80 80 00 00 07 96 80 80 80 00 02 06 6D 65
5 6D 6F 72 79 02 00 09 5F 5A 35 61 64 64 34 32 69
6 00 00 0A 8D 80 80 80 00 01 87 80 80 80 00 00 20
7 00 41 2A 6A 0B
该文件是 WebAssembly 模块,它可以加载到 JavaScript 中使用,现阶段加载的过程稍微有点复杂。
1 function fetchAndInstantiate(url, importObject) {
2 return fetch(url).then(response =>
3 response.arrayBuffer()
4 ).then(bytes =>
5 WebAssembly.instantiate(bytes, importObject)
6 ).then(results =>
7 results.instance
8 );
9 }
3.如何使用
WebAssembly生态系统处在初始阶段;更多的工具会毫无疑问得不断出现。当然,有两个主要的着手点:
- 使用Emscripten移植一个C/C++应用程序。
- 直接在汇编层,编写或生成WebAssembly代码。
- 编写Rust程序,将WebAssembly作为它的输出。
从一名C++程序员使用角度来看,那么大致流程如下:
Emscripten工具能够将一段C/C++代码,编译出:
- 一个.wasm模块
- 用来加载和运行该模块的JavaScript”胶水“代码
- 一个用来展示代码运行结果的HTML文档
简而言之,工作流程如下所示:
-
Emscripten首先把C/C++提供给clang+LLVM——一个成熟的开源C/C++编译器工具链,比如,在OSX上是XCode的一部分。
-
Emscripten将clang+LLVM编译的结果转换为一个.wasm二进制文件。
-
就自身而言,WebAssembly当前不能直接的存取DOM;它只能调用JavaScript,并且只能传入整形和浮点型的原始数据类型作为参数。这就是说,为了使用任何Web API,WebAssembly需要调用到JavaScript,然后由JavaScript调用Web API。因此,Emscripten创建了HTML和JavaScript胶水代码以便完成这些功能。
如果上面你还是看不明白或者觉得很复杂,那么我实际告诉你,其实就分为以下两步:
- 1.使用Emscripten编译C++代码,会同时生成wasm文件、.js文件(胶水代码)、和一个html文件(实际使用中肯定不用它自己生成的)
- 2.使用JavaScript将js文件进行加载
五、使用注意
WebAssebmly一切看起来都很美好,但是它想将所有的编程语言编译的wasm文件放入浏览器难道就不会有问题吗?
C++方面来看,下面是使用时需要注意的:
1.使用C风格符号修饰。
我们知道,由于引入了多态、重载、模板等特性,C语言环境下的符号修饰策略(既函数、变量在最终编译成果中的名字的生成规则)非常复杂,并且不同的C编译器有着各自的符号修饰策略,如果不做额外处理,我们在C中创建函数的时候,很难预知它在最终编译成果中的名字——这与C语言环境完全不同。
因此当我们试图将main()函数之外的全局函数导出至JavaScript时,必须强制使用C风格的符号修饰,以保持函数名称在C/C环境以及JavaScript环境中有统一的对应规则。
2. 优化
我们知道很多C++编译器都会进行优化代码的,避免函数因为缺乏引用而导致在编译时被优化器删除。
如果某个导出函数仅供JavaScript调用,而在C/C++环境中从未被使用,开启某些优化选项(比如-O2以上)时,函数有可能被编译器优化删除,因此需要提前告知编译器:该函数必须保留,不能删除,不能改名。
3.兼容性
为了保持足够的兼容性,宏需要根据不同的环境——原生NativeCode环境与Emscripten环境、纯C环境与C++环境等——自动切换合适的行为
4. 线程问题
(待补充)
六、性能对比
这一段感谢姬无华博主,摘自他的部分整理,他的那边文章是专门介绍性能的。
在我们了解 JavaScript 和 WebAssembly 的性能区别之前,需要先理解 JS 引擎的工作原理,如果不想了解这些可以略过。
这张图大致给出了现在一个程序的启动性能,目前 JIT 编译器在浏览器中很常见。
JS 引擎在图中各个部分所花的时间取决于页面所用的 JavaScript 代码。图表中的比例并不代表真实情况下的确切比例情况。
图中的每一个颜色条都代表了不同的任务:
- Parsing——表示把源代码变成解释器可以运行的代码所花的时间;
- Compiling + optimizing——表示基线编译器和优化编译器花的时间。一些优化编译器的工作并不在主线程运行,不包含在这里。
- Re-optimizing——当 JIT 发现优化假设错误,丢弃优化代码所花的时间。包括重优化的时间、抛弃并返回到基线编译器的时间。
- Execution——执行代码的时间。
- Garbage collection——垃圾回收,清理内存的时间。
这里注意:这些任务并不是离散执行的,或者按固定顺序依次执行的。而是交叉执行,比如正在进行解析过程时,其他一些代码正在运行,而另一些正在编译。
这样的交叉执行给早期 JavaScript 带来了很大的效率提升,早期的 JavaScript 执行类似于下图,各个过程顺序进行:
早期时,JavaScript 只有解释器,执行起来非常慢。当引入了 JIT 后,大大提升了执行效率,缩短了执行时间。
JIT 所付出的开销是对代码的监视和编译时间。JavaScript 开发者可以像以前那样开发 JavaScript 程序,而同样的程序,解析和编译的时间也大大缩短。这就使得开发者们更加倾向于开发更复杂的 JavaScript 应用。
同时,这也说明了执行效率上还有很大的提升空间。
WebAssembly 对比
下面是 WebAssembly 和典型的 web 应用的近似对比图: