前言
如果还不了解WebAssembly是什么的,可以查阅我的另一篇博客:走进WebAssembly。本节内容主要探讨WebAssembly相比于js的执行效率究竟如何,并且定论适不适合应运于项目中,以及适合应用于哪类项目。当然,本节依旧采用rust语言编译WebAssembly。
case1:循环+累加
现有如下三种方式 js+for、js+while、wasm+for,我们将在循环次数为十、千、十万、千万的场景中分别各自执行五次,对比其结果。
js+for
const js_for = () => {
let v = 1;
for(let i = 0; i < 10; i++){
v += i;
};
return v
}
console.time('js-for')
js_for();
console.timeEnd('js-for')
js+while
const js_while = () => {
let i = 0
let v = 1;
while(i < 10){
v += i;
i++
}
return v
}
console.time('js-while')
js_while();
console.timeEnd('js-while')
wasm+for
import init, { wasm_for } from "../layout/pkg/layout.js";
init().then(() => {
console.time('wasm-for')
wasm_for();
console.timeEnd('wasm-for')
});
#[wasm_bindgen]
pub fn wasm_for() {
let mut v = 1;
for i in 0..10{
v += i;
};
}
结果对比(以下数据单位均为毫秒ms)
循环十次 | js+for | js+while | wasm+for |
---|---|---|---|
第一次 | 0.02392578125 | 0.02001953125 | 0.01416015625 |
第二次 | 0.007080078125 | 0.0029296875 | 0.011962890625 |
第三次 | 0.0029296875 | 0.001953125 | 0.01904296875 |
第四次 | 0.0048828125 | 0.004150390625 | 0.02001953125 |
第五次 | 0.009033203125 | 0.0029296875 | 0.011962890625 |
平均 | 0.0095703125 | 0.006396484375 | 0.0154296875 |
循环千次 | js+for | js+while | wasm+for |
---|---|---|---|
第一次 | 0.02880859375 | 0.02392578125 | 0.01513671875 |
第二次 | 0.008056640625 | 0.00732421875 | 0.01513671875 |
第三次 | 0.016845703125 | 0.01318359375 | 0.0107421875 |
第四次 | 0.01416015625 | 0.010009765625 | 0.012939453125 |
第五次 | 0.008056640625 | 0.0068359375 | 0.030029296875 |
平均 | 0.015185546875 | 0.012255859375 | 0.016796875 |
循环十万次 | js+for | js+while | wasm+for |
---|---|---|---|
第一次 | 2.894287109375 | 2.691162109375 | 0.013916015625 |
第二次 | 1.18994140625 | 1.235107421875 | 0.010986328125 |
第三次 | 1.009765625 | 1.367919921875 | 0.010986328125 |
第四次 | 0.968994140625 | 1.16015625 | 0.010986328125 |
第五次 | 1.119140625 | 1.23291015625 | 0.011962890625 |
平均 | 1.43642578125 | 1.537451171875 | 0.011767578125 |
循环千万次 | js+for | js+while | wasm+for |
---|---|---|---|
第一次 | 8.430908203125 | 29.019775390625 | 0.01708984375 |
第二次 | 8.338134765625 | 8.43505859375 | 0.010986328125 |
第三次 | 8.72998046875 | 8.52294921875 | 0.01904296875 |
第四次 | 8.714111328125 | 32.125244140625 | 0.018310546875 |
第五次 | 8.976806640625 | 8.64111328125 | 0.01318359375 |
平均 | 8.63798828125 | 17.348828125 | 0.01572265625 |
将以上数据均值归入折线图对比,可明显发现循环次数越大时,js执行耗时耗时越大,而WebAssembly似乎并受到循环次数增加而增加其运行时间。由此可见当循环次数越大时,WebAssembly非常优于js。
如果你仔细看了上述数据,可以发现在循环千万次的js+while中,运行时长波动极大,故而可以给自己留一个心眼。
既然WebAssembly这么快,那为什么还没有普及?不急,继续往下看。
case2:传参循环
在case1中我们有看到WebAssembly的强大,但在实际开发过程中我们是无法离开js的,要想使用WebAssembly,需要我们将js中的数据传入到WebAssembly的方法中处理,并返回。
ok,现有如下四种方式 js+for、js+while、js+forEach、wasm+for,我们这次直接查看十万次数据对比。
函数入参生成
const rand = (n: number, m: number) => {
var c = m - n + 1
return Math.floor(Math.random() * c + n)
}
let arr: Array<{x: number, y: number}> = []
for(let i = 0; i < 10000000; i++){
arr.push({
x: rand(0, 10),
y: rand(0, 10)
})
};
js+for
const js_for = (arr: Array<{x: number, y: number}>) => {
let x = 0;
let y = 0;
let length = arr.length
for(let i = 0; i < length; i++){
let arrI = arr[i]
if(arrI){
x += arrI.x
y += arrI.y
}
};
console.log('js_for', x, y)
}
console.time('js_for')
js_for(arr);
console.timeEnd('js_for')
js+while
const js_while = (arr: Array<{x: number, y: number}>) => {
let x = 0;
let y = 0;
let length = arr.length
let i = 0
while(i < length){
let arrI = arr[i]
if(arrI){
x += arrI.x
y += arrI.y
}
i++
}
console.log('js_while', x, y)
}
console.time('js_while')
js_while(arr);
console.timeEnd('js_while')
js+forEach
const js_forEach = (arr: Array<{x: number, y: number}>) => {
let x = 0;
let y = 0;
arr.forEach(item => {
if(item){
x += item.x
y += item.y
}
})
console.log('js_forEach', x, y)
}
console.time('js_forEach')
js_forEach(arr);
console.timeEnd('js_forEach')
wasm+for
import init, { wasm_for } from "../layout/pkg/layout.js";
init().then(() => {
console.time('wasm_for')
wasm_for();
console.timeEnd('wasm_for')
});
#[derive(Serialize, Deserialize)]
struct Point {
x: i32,
y: i32,
}
#[wasm_bindgen]
pub fn wasm_for(arr: JsValue) -> Result<JsValue, JsValue>{
let arr: Vec<Point> = from_value(arr).unwrap();
let mut x = 0;
let mut y = 0;
for i in 0..arr.len() {
if let Some(arr_i) = arr.get(i) {
x += arr_i.x;
y += arr_i.y;
}
}
let result = Point { x: x, y: y };
Ok(to_value(&result).unwrap())
}
循环千万次 | js+for | js+while | js+forEach | wasm+for |
---|---|---|---|---|
第一次 | 53.799072265625 | 49.403076171875 | 183.83984375 | 8930.927978515625 |
第二次 | 44.261962890625 | 45.554931640625 | 149.68603515625 | 8388.2861328125 |
第三次 | 43.518798828125 | 45.261962890625 | 132.517822265625 | 8432.029052734375 |
看到以上数据时,不禁大为吃惊,WebAssembly为什么执行时间会这么大,甚至已经没有了让我求平均值得欲望,我尝试将循环次数降为十次后的数据如下
循环十次 | js+for | js+while | js+forEach | wasm+for |
---|---|---|---|---|
第一次 | 0.17822265625 | 0.111083984375 | 0.125732421875 | 0.5908203125 |
第二次 | 0.1669921875 | 0.130859375 | 0.0947265625 | 0.93994140625 |
第三次 | 0.08203125 | 0.041015625 | 0.119140625 | 0.60498046875 |
没有任何意外,WebAssembly的执行时间仍然是js执行时间的数倍,那与case1相比区别在何处?没错,就是“let arr: Vec = from_value(arr).unwrap();”这句话。在rust中是无法直接处理js数组对象类数据的,这句话的目的就是将js数据转换为rust可以识别的数据,而性能也恰恰最大的消耗在“无用”的数据转换上,WebAssembly与js的通信桥梁直接崩裂。我只能说如果这个问题无法解决,那将很难走进web。
这还仅仅是js数据转换rust数据,如果rust数据再转换js数据,那执行时间是不是要翻倍呢?
结论
WebAssembly快吗?快。WebAssembly能用吗?不能用。
以上观点仅限本人,如有大哥有提议,可以评论区留言探讨一下啦。
在web上WebAssembly还有适用的场景吗?我只能想到在有大量数据处理+不频繁数据转换的项目才可能使用,但是有这样的项目吗?
相关知识
在case2的中rust代码中需要引入以下代码
use serde::{Deserialize, Serialize};
use serde_wasm_bindgen::{from_value, to_value};
并且修改Cargo.toml配置
[dependencies]
wasm-bindgen = "0.2"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
serde-wasm-bindgen = "0.3"