综合开发

[WebAssembly]浏览器内,javascript之外的速度

微信扫一扫,分享到朋友圈

[WebAssembly]浏览器内,javascript之外的速度

经过这几天对Webassembly的学习,我觉得我的C语言功底已经达到了筑基期(大雾

2019年12月,随着W3C正式宣布,WebAssembly正式成为新标准,我对WebAssembly的学习随之展开。

WebAssembly,顾名思义,就是“网页”、“汇编”,即在网页跑汇编程序的能力。这个词,很早之前就已经传遍了前端圈子,无论是阮一峰老师的博客里热情洋溢的介绍,还是朋友圈里面看见其他语言编译到浏览器执行时“失业”的担忧,亦或是圈子里《你需要/不需要WebAssembly》的争论。总之,你不下场研究学习它,即使认识再深刻,始终还是会觉得隔了层纱。

于是我下场研究学习了,写三个小小的例子,欢迎大家把玩: github.com/Char-Ten/we…

所有例子都是基于emscripten这个工具构建的,它能够支持c/c++编译到WebAssembly环境,进而在浏览器运行。对于这个工具的安装和使用,网上已经有诸多介绍,这里不再展开。唯一想说的,便是想提醒大家,安装emscripten的时候,一定一定一定要带好梯子科学上网!

当然业界还有另外一个选择:AssemblyScript,这个安装简单而且用typescript的语法,按理来说应该是前端入门的首选。但是我并不推荐用它来入门,WebAssembly是一个很底层的环境,而ts的语法抽象得太高级了,因此很难通过它了解WebAssembly的运行方式。其次,AssemblyScript很难打出“裸包”,也就是不带js胶水代码的wasm包,高度封装,你很难学习WebAssembly的API。最后,AssemblyScript如果跑不起来,很难谷歌到具体的原因。

下面简单介绍一下这三个例子。

0x01 c语言简单加法

第一个是最基础的,就是运行一个加法程序:

int add(int a,int b){
return a+b
}
复制代码

然后js调用它:

WebAssembly
.instantiate(mod,{env}) //mod是编译好的wasm包,env是WebAssembly的环境配置
.then(ins=>
console.log(ins.export.add(1,2)) // 输出 3
)
复制代码

你会在很多介绍WebAssembly的博客、文章、教程里面看见这个例子,不错,我也是抄过来的。这个例子仅仅演示了c语言能通过WebAssembly在浏览器上跑,并不能演示更多内容。很多教程里也是介绍到这里截然而止,导致学习起来十分难受。

好在MDN给了第二个例子,数组累加,于是我也就把它抄了过来:

int test(int *p,int len){
int r = 0;
for(int i =0 ;i<len;i++){
r+=*p;
p++;
};
return r;
};
复制代码

然后在js里面划分内存,写入数据:

//类型数组传递 js函数调用
let i32a = new Int32Array(env.memory.buffer);//env.memory.buffer是webassembly的内存,可以直接访问
var r = 0;
for(var i = 0;i<10000;i++){
i32a[i]=Math.ceil(Math.random()*100);
r+=i32a[i];
}
console.log("js计算结果:",r);
console.log("wasm计算结果:",ins.exports.test(0,10000));
复制代码

env.memory 是分配给WebAssembly的内存,它就是 WebAssembly.Memory 的实例。js通过上面的代码访问内存,把数据写进去,然后给c传递一个指针地址和长度,即可让c处理大量的数据! 于是我刹那间顿悟出一个结论:

JS和WebAssembly的数据传递,通过 WebAssembly.Memory ,也就是对WebAssembly内存的读写来完成。

基于此,联想到c语言里面,字符串都是数组,因此我猜测字符串的传递也应该是通过内存的读写来进行,而非像js一样当成一个值来传递。于是我写下新的例子用来验证。

0x02 字符串交互

交互,自然便有两种,一种是WebAssembly生成字符串,然后传递给js;另一种则是js将字符串传递给WebAssembly用来计算。

先是用c来生成字符串传过去。

char *myCharAt()
{
char txt[] = "test 大家好 你好世界";
return txt;
};
复制代码

C代码也很简单,就是创建一个字符串,然后返回它的首位指针。JS接收后把其打印到页面上即可:

function printCharToPage() {
let str = "";
let i = mod._myCharAt();
while (mod.HEAP8[i] !== 0) {
str += `%${mod.HEAPU8[i].toString(16)}`;
i++;
}
document.body.appendChild(
document.createTextNode(decodeURIComponent(str))
);
}
复制代码

这里js代码里的 mod 变量,是由emscripten的胶水代码导出来的一个对象。通过执行 mod._myCharAt 函数,调用c代码,拿到指针地址 i ,因为是 char 型指针,所以通过遍历emscripten胶水代码提供的 mod.HEAP8 类型数组,把所有字符串拼接起来。值得注意的是,js的字符集是 utf-16 ,c导出的代码是 utf-8 ,所以这里利用 decodeURIComponent ,取巧做个简单的解码转换,然后把字符串打印到页面上。

然后是js传字符串给c代码执行。首先js的字符串生成:

inp.addEventListener("input", function() {
let utf8Array = new Uint8Array([...encodeUtf8(inp.value),0]);
// 分配utf-8内存
let p = mod._malloc(utf8Array.length*utf8Array.BYTES_PER_ELEMENT);
mod.HEAPU8.set(utf8Array,p);
let v = mod._stringLen(p);
// 输出字节数量
otp.value = ` utf-8 bytes length:${v}`;
mod._free(p);
});
复制代码

这里加了简单的页面交互,通过一个input框输入随意的值,然后把它转为 utf-8 ,然后写入一个 Uint8Array 的类型数组里,注意c语言里的字符串都是0结尾的,所以多写一个0进去。然后是用胶水代码导出的工具函数 mod._malloc 分配内存,然后同样的把数据写进 mod.HEAPU8 这个类型数组里,也就是写进webassembly的内存。最后就可以调用c的函数 _stringLen 了。

原本我是想写一个像vue官方例子那样把字符串倒排,不过在c里面,发现一个 utf-8 字符并不是只占固定字节的,自己也懒得去写一个 utf-8 解析的工具函数,也懒得找库,倒序写起来很麻烦,所以就简简单单写一个统计 utf-8 字节数的功能吧:

#include <string.h>
size_t stringLen(const char *p)
{
return strlen(p);
};
复制代码

最后记得在js里面,把申请的内存释放掉,以免造成泄漏。

最后有请图像处理中最简单最实用的老朋友,用来提取图像边缘的算法:sobel。

0x03 用sobel来测试速度。

先简单介绍下sobel算法。

sobel算法是非常简单的,就是遍历图像的每一个像素 ,然后取像素周围8个像素的灰度值,记为

;

;

则 点的灰度为

;

这样就把边缘提取出来了,原理也很简单,假设P点没有在边缘的位置,那P点周围的灰度肯定都是一样的,那Gx和Gy计算后都是0,那P就是黑色了,反正则是灰色。(当然会有误差,存在两个颜色灰度相同的情况,但在这里并不是重点…)

所以我们看看复杂度,无论如何,一张图片的像素都需要遍历到,然后再加上周围8个像素,所以算法复杂度就是 。

我自己准备了一张5632*2048分辨率的示例图,是从P社4萌之一《欧陆风云4》的游戏目录里面扒的。它用纯色块来表示游戏里的每一个地块(省份),非常适合用来做边缘检测的例图(无噪点效果好)。

于是你们就可以看见上面这张图,在我的32G内存里,js跑了近4秒,而Webassembly只用约0.1~0.2秒,差距非常大。当然,Webassembly还是跑在cpu里面的,对于图像的运算,再快也不可能快过gpu,webgl跑出了0.03秒的速度,而且随着把纹理写入缓存后,这个速度还能提高。

当然Webassembly快虽快,但是在我的例子中还是有瑕疵的,首先是它的误差问题,我是用整型计算,因为这样用位运算取值非常方便,性能也好,但缺点是误差高,比如一个颜色的灰度值是94.8,另一个是95.2,但整型后就都变成了95。js慢归慢,但好歹float运算保留了小数点(即使没有小数点也会给你带上…),因此误差小。当然这个问题归根到底还是我个人的能力问题。

其次是,在调试过程中发现,WebAssembly运行期间,访问js函数的次数越少,性能越佳。最开始为了节省内存开销,图片像素的灰度值是通过调用js函数获得的,这样WebAssembly只需要申请 (R、G、B、A各一个字节)个字节的内存就可以了。于是每次遍历都调用js函数取1次灰度值,最终运行速度为2秒左右。后面改了,直接把原图读到内存里面,直接通过指针读像素灰度值,速度立马提起来。

所以说,那些大型应用想移植到浏览器里,最终性能还是卡在js手里,想想还真是好气啊。

好了,感谢大家阅读到这里,后面我会继续看下去,比如跑一下大型库之类的,还有 WebAssembly.TableWebAssembly.Global 这两个API的使用,同样也会写例子放上去。这些例子,我个人觉得比MDN上的好一点,至少我把源码、打包命令、js代码都放在一起了(深深怨念)。希望能够帮助大家,有兴趣的同学也可以把仓库clone到本地改改代码玩一下,当然能够给个好评给个赞给个star就更好了。

Linux创建用户配置sudo权限

上一篇

[图]斯诺登:疫情期间政府所用高科技监视措施会产生深远影响

下一篇

你也可能喜欢

评论已经被关闭。

插入图片

热门栏目

[WebAssembly]浏览器内,javascript之外的速度

长按储存图像,分享给朋友