WebAssembly的简单实践


浏览器的性能

JavaScript一开始就是动态类型解释性语言,动态类型解释性语言的一大特点就是灵活和慢。

所以JavaScript和所有的动态类型语言一样,天生会比静态类型语言慢。

而随着网页应用越来越复杂,JavaScript的运行性能就必须跟着提高。

那么为什么动态类型语言,如PythonPHPJavaScript就会比C/C++等静态类型语言慢呢?

JavaScript慢在哪里

我们来看一个最简单的情况,实现c = a + b加法运算,如果是C/C++语言,实现步骤大致如下:

  1. 内存a里取值到寄存器
  2. 内存b里取值到寄存器
  3. 算加法
  4. 把结果放到内存c

如果是JavaScript大致会经历哪些步骤呢?

  1. 代码编译
  2. 当前上下文是否有变量a,没有的话去上一层寻找,直到找到,或到达最外层上下文
  3. 当前上下文是否有变量b,没有的话去上一层寻找,直到找到,或到达最外层上下文
  4. 判断变量a的变量类型
  5. 判断变量b的变量类型
  6. 变量a、变量b是否需要进行类型转化,并进行类型转化
  7. 算加法
  8. 运行结果赋值给c

我们可以看到,二者的整个流程还是有比较大的区别

浏览器的性能填坑

1. JIT

JIT(just-in-time compilation):如果在执行c = a + b的时候,ab几乎都是int类型,那么是否可以去掉类型判断,类型转化的步骤,用接近C/C++的方式来实现加法运算,并把执行代码直接编译成机器码,直接运行,不需要再次编译。

Google 在 2009 年在 V8 中引入了 JIT 技术,JavaScript的执行速度瞬间提升了 20 - 40 倍的速度。
JIT的问题是并不是所有的代码都能得到很好的提升,因为JIT基于运行期分析编译,而JavaScript是一个没有类型的语言,所以当代码中的类型经常变化的时候,性能提升是有限的。
比如

function add (a, b)
{
    return a + b
}
var c = add(1, 2);

JIT 看到这里, 觉得好开心, 马上把 add编译成

function add (int a, int b)
{
    return a + b
}

但是,很有可能,后面的代码是这样的

var c = add("hello", "world");

JIT 编译器的可能当时就哭了,因为add已经编译成机器码了,只能推到重来

2. asm.js

2012年,Mozilla 的工程师 Alon Zakai 在研究LLVM编译器时突发奇想:许多 3D 游戏都是用 C / C++语言写的,如果能将 C / C++语言编译成 JavaScript 代码,它们不就能在浏览器里运行了吗?

于是,他开始研究怎么才能实现这个目标,为此专门做了一个编译器项目Emscripten。这个编译器可以将 C / C++代码编译成 JS代码,但不是普通的JS,而是一种叫做 asm.jsJavaScript变体。

asm.js它的变量一律都是静态类型,并且取消垃圾回收机制。当浏览器的JavaScript引擎发现运行的是 asm.js时,就会跳过语法分析这一步,将其转成汇编语言执行。asm.js的执行速度可以达到原生代码的50%

asm.js的一般工作流程为:

C/C++
LLVM
Emscripten
JavaScript

asm.js还是存在几个问题:

  1. 仅有FirFox的浏览器有良好的支持
  2. 代码传输还是与现有方式一样,传输源码,本地编译

3. WebAssembly

Mozilla,Google,Microsoft, 和Apple 觉得 Asm.js 这个方法有前途,想标准化一下,大家都能用。
便诞生了WebAssembly

有了大佬们的支持,WebAssemblyasm.js要激进很多。 WebAssembly连编译 JS这种事情都懒得做了,不是要 AOT吗? 我直接给字节码好不好?(后来改成 AST 树)。对于不支持 Web Assembly的浏览器, 会有一段JavaScriptWeb Assembly重新翻译为 JavaScript运行。

2019年12月5日,万维网联盟(W3C)宣布 WebAssembly成为正式标准

什么是WebAssembly

  • WebAssembly是一种运行在现代网络浏览器中的新型代码,并且提供新的性能特性和效果。
  • 它设计的目的是为诸如C、C++Rust等低级源语言提供一个高效的编译目标。

WebAssembly的目标

  • 高性能——能够以接近本地速度运行。
  • 可移植——能够在不同硬件平台和操作系统上运行。
  • 保密——WebAssembly是一门低阶语言,是一种紧凑的二进制格式,具有良好的保密性。
  • 安全——WebAssembly被限制运行在一个安全的沙箱执行环境中。像其他网络代码一样,它遵循浏览器的同源策略和授权策略。
  • 兼容——WebAssembly的设计原则是与其他网络技术和谐共处并保持向后兼容。

浏览器兼容性

兼容性

使用方法

官网有非常详细的使用说明
官网
MDN

1.安装依赖(Ubuntu 20 .04)

sudo apt install python3
sudo apt install cmake

2. 安装Emscripten

git clone https://github.com/emscripten-core/emsdk.git
cd emsdk
git pull
./emsdk install latest
./emsdk activate latest
source ./emsdk_env.sh

3. Hello World

创建一个文件hello.c:

#include <stdio.h>
int main() {
  printf("Hello, WebAssembly!\n");
  return 0;
}

编译C/C++代码:

emcc hello.c -s WASM=1 -o hello-wasm.html 

会生成三个文件:hello-wasm.html, hello-wasm.js, hello-wasm.wasm,然后浏览器打开hello-wasm.html,就能看到输出。

4. 调用C/C++函数

  1. 创建一个文件add.cpp:
extern "C" {

int add(int a, int b) {
  return a + b;
}

}
  1. 执行编译
emcc add.cpp -o add.html -s EXPORTED_FUNCTIONS='["_add"]' -s EXPORTED_RUNTIME_METHODS='["ccall","cwrap"]'

这里的EXPORTED_FUNCTIONS参数指定需要暴露的函数接口名字,需要在名字前单独加一个下划线_EXPORTED_RUNTIME_METHODS指定可以被调用的方式

使用cwrap的方式调用,在生成的add.html中加入如下代码:

Module.onRuntimeInitialized = () => { 
    add = Module.cwrap('add', 'number', ['number', 'number']);
    result = add(9, 9);
}

因为调用对应的C/C++接口前,还需要先初始化,所以要在Module.onRuntimeInitialized事件后,才能通过JS调用C/C++的内容

5.测试性能

我们再实现一个JavaScript版本的加法函数

function js_add(a, b) {
	return a + b;
}

分别调用1000000次,对比分别的耗时,实现代码如下:

Module.onRuntimeInitialized = () => { 
    add = Module.cwrap('add', 'number', ['number', 'number']);
    const count = 1000000;
    let result;
    	
    console.time("js call");
    for (let i = 0; i < count; i ++) {
    	result = js_add(9, 9);
    }
    console.timeEnd("js call");
    	
    console.time("c call");
    for (let i = 0; i < count; i ++) {
    	result = add(9, 9);
    }
    console.timeEnd("c call");
}

大家觉得哪个更快?为什么?

现实可能和我们想象的不一样,在多次调用后,JavaScript的调用速度反而更快。

在这里插入图片描述

这是为什么呢?

其实是在我们多次调用JS函数时,由于多次调用输入,输出参数都是同样的类型,所以V8引擎会自动的优化我们的代码,

而我们调用WebAssembly的模块代码,中间的传输还需要一定时间,如果调用次数很多,中间的传输过程需要的时间就更多了,

所以会出现JavaScript的调用更快的情况。

6.另一个测试

这次换一个思路,直接在C中实现累加,修改上一步的add.cpp,并保存为add_all.cpp

extern "C" {

long add_all(int count) {
    long result = 0;
    for(int i = 0; i < count; i++){
        result += i;
    }
    return result;
}
}

用同样的命令进行编译

emcc add_all.cpp -o add_all.html -s EXPORTED_FUNCTIONS='["_add_all"]' -s EXPORTED_RUNTIME_METHODS='["ccall","cwrap"]'

我们再实现一个JavaScript版本的js_add_all

function js_add_all(count) {
    let result = 0;
    for(let i = 0; i < count; i++){
    	result += i;
    }
    return result;
}

然后进行运行测试:

Module.onRuntimeInitialized = () => { 
    add_all = Module.cwrap('add_all', 'number', ['number']);
   	const count = 50000;
    	
   	console.time("js call");
   	console.log(js_add_all(count));
   	console.timeEnd("js call");
   	
   	console.time("c call");
    console.log(add_all(count));
   	console.timeEnd("c call");   	
}

这次谁更快?当count = 100000时会怎么样?为什么?
在这里插入图片描述

这次我们就可以看到明显的速度差异了

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值