浏览器的性能
JavaScript
一开始就是动态类型解释性语言,动态类型解释性语言的一大特点就是灵活和慢。
所以JavaScript
和所有的动态类型语言一样,天生会比静态类型语言慢。
而随着网页应用越来越复杂,JavaScript
的运行性能就必须跟着提高。
那么为什么动态类型语言,如Python
、PHP
、JavaScript
就会比C/C++
等静态类型语言慢呢?
JavaScript
慢在哪里
我们来看一个最简单的情况,实现c = a + b
加法运算,如果是C/C++
语言,实现步骤大致如下:
- 内存a里取值到寄存器
- 内存b里取值到寄存器
- 算加法
- 把结果放到内存c
如果是JavaScript
大致会经历哪些步骤呢?
- 代码编译
- 当前上下文是否有变量a,没有的话去上一层寻找,直到找到,或到达最外层上下文
- 当前上下文是否有变量b,没有的话去上一层寻找,直到找到,或到达最外层上下文
- 判断变量a的变量类型
- 判断变量b的变量类型
- 变量a、变量b是否需要进行类型转化,并进行类型转化
- 算加法
- 运行结果赋值给c
我们可以看到,二者的整个流程还是有比较大的区别
浏览器的性能填坑
1. JIT
JIT
(just-in-time compilation):如果在执行c = a + b
的时候,a
和b
几乎都是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.js
的 JavaScript
变体。
asm.js
它的变量一律都是静态类型,并且取消垃圾回收机制。当浏览器的JavaScript
引擎发现运行的是 asm.js
时,就会跳过语法分析这一步,将其转成汇编语言执行。asm.js
的执行速度可以达到原生代码的50%。
asm.js
的一般工作流程为:
但asm.js
还是存在几个问题:
- 仅有
FirFox
的浏览器有良好的支持 - 代码传输还是与现有方式一样,传输源码,本地编译
3. WebAssembly
Mozilla,Google,Microsoft, 和Apple 觉得 Asm.js 这个方法有前途,想标准化一下,大家都能用。
便诞生了WebAssembly
。
有了大佬们的支持,WebAssembly
比 asm.js
要激进很多。 WebAssembly
连编译 JS
这种事情都懒得做了,不是要 AOT
吗? 我直接给字节码好不好?(后来改成 AST 树)。对于不支持 Web Assembly
的浏览器, 会有一段JavaScript
把 Web Assembly
重新翻译为 JavaScript
运行。
2019年12月5日,万维网联盟(W3C
)宣布 WebAssembly
成为正式标准
什么是WebAssembly
-
WebAssembly
是一种运行在现代网络浏览器中的新型代码,并且提供新的性能特性和效果。 - 它设计的目的是为诸如
C、C++
和Rust
等低级源语言提供一个高效的编译目标。
WebAssembly
的目标
- 高性能——能够以接近本地速度运行。
- 可移植——能够在不同硬件平台和操作系统上运行。
-
保密——
WebAssembly
是一门低阶语言,是一种紧凑的二进制格式,具有良好的保密性。 -
安全——
WebAssembly
被限制运行在一个安全的沙箱执行环境中。像其他网络代码一样,它遵循浏览器的同源策略和授权策略。 -
兼容——
WebAssembly
的设计原则是与其他网络技术和谐共处并保持向后兼容。
浏览器兼容性
使用方法
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++函数
- 创建一个文件add.cpp:
extern "C" {
int add(int a, int b) {
return a + b;
}
}
- 执行编译
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时会怎么样?为什么?
这次我们就可以看到明显的速度差异了