前言
WebAssembly 不用多说懂的都懂,将运算函数通过 c++ 等编译为二进制的 .wasm
文件后,再通过 JavaScript 的 WebAssembly Api 调用即可进行“快速”计算。
下面快速上手体验一把,不使用 c++ 等编译 .wasm
文件。
WebAssembly 中文网:WebAssembly-CN
WebAssembly 英文官网:WebAssembly-EN
使用
AssemblyScript
AssemblyScript 是 WebAssembly 社区的一个 JavaScript 解决方案,通过编写 TypeScript 来达到近似 C 语言的类型限制,完成预编译,进而做到编译出二进制 .wasm
文件。
AssemblyScript 官方项目:AssemblyScript / assemblyscript
那他与使用 c++ 等进行编译有什么限制呢,主要是两点:
-
类型不全面。因为是通过 ts 给出近似实现,所以目前不可以使用复杂类型(比如
RegExp
,TextEncoder
等),且 h5 新 api 大部分也不能使用。 -
不可以使用第三方依赖。
对于第一点类型限制,由于是使用 ts 编写所以不支持的类型会报错还是很友好的,对于第二点,意味着所有功能都需要我们纯手撕,另外很多 h5 api 用不了的情况下,更增加了复杂性。
搭建环境
官方文档:AssemblyScript
先初始化项目:
yarn init -y
安装两个基本依赖:
yarn add @assemblyscript/loader
yarn add -D assemblyscript
其中 @assemblyscript/loader
是微型加载器,可以帮我们省去很多配置,即开即用。assemblyscript
是开发核心,内置了所有可用的类型声明(在 node_modules/assemblyscript/std
下)。
初始化项目:
yarn asinit .
此时会打印将要生成的文件结构,输入 Y
确认,将得到一个基础开发目录:
我们关注的只是在 assembly/index.ts
内函数编写逻辑。
base64 逻辑实现
虽然 base64 是个小功能,但是痛点啪的一下就凸显出来了,很快啊。
我们不能使用第三方库,于是乎 js-base64
是不能用的。去把源码搬进来行不行?是不行的,因为 js-base64
使用了很多 h5 的 api 与 RegExp
正则替换,所以我们只能实现一个 乞丐版 的:编码 base64:
// ./assembly/index.ts
export class Base64 {
_keyStr: string = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
encode(input: string): string {
var output = "";
var chr1: i32, chr2: i32, chr3: i32, enc1: i32, enc2: i32, enc3: i32, enc4: i32;
var i = 0;
input = this._utf8_encode(input);
while (i < input.length) {
chr1 = input.charCodeAt(i++);
chr2 = input.charCodeAt(i++);
chr3 = input.charCodeAt(i++);
enc1 = chr1 >> 2;
enc2 = ((chr1 & 3) << 4) | (chr2 >> 4);
enc3 = ((chr2 & 15) << 2) | (chr3 >> 6);
enc4 = chr3 & 63;
if (isNaN(chr2)) {
enc3 = enc4 = 64;
}
else if (isNaN(chr3)) {
enc4 = 64;
}
output = output +
this._keyStr.charAt(enc1) + this._keyStr.charAt(enc2) +
this._keyStr.charAt(enc3) + this._keyStr.charAt(enc4);
} // Whend
return output;
} // End Function encode
_utf8_encode(string: string): string {
var utftext = "";
for (var n = 0; n < string.length; n++) {
var c = string.charCodeAt(n);
if (c < 128) {
utftext += String.fromCharCode(c);
}
else if ((c > 127) && (c < 2048)) {
utftext += String.fromCharCode((c >> 6) | 192);
utftext += String.fromCharCode((c & 63) | 128);
}
else {
utftext += String.fromCharCode((c >> 12) | 224);
utftext += String.fromCharCode(((c >> 6) & 63) | 128);
utftext += String.fromCharCode((c & 63) | 128);
}
} // Next n
return utftext;
}
}
export function getBase64(): Base64 {
return new Base64()
}
语法规则
编写 AssemblyScript 就要遵守他的规则,我们着重需要确定的是三个部分:
-
回归原始。不使用 h5 新 api (
atob
、TextEncoder
等),不接触复杂类型(RegExp
等),如果编辑器红线报错就是找不到相对应的类型了,说明这个类型无法使用。 -
注意数字类型。对于 number 类型,AssemblyScript 含有更具体的
i32
、i64
、f32
等类型(见官方文档 Types ),在相应的变量初始化时对其指定合适的类型。 -
明确基本类型。变量初始化要指明其基础类型,以便 AssemblyScript 预编译。
到此为止,我们完成了最核心的 AssemblyScript 逻辑编写。
此处需要我们注意两个问题:
-
上面这个逻辑是简单版的,比如换行等边界情况是没有考虑的。
-
为什么要写成单例模式,这么写是官方推荐的导出类写法,采用相同的引用节省资源。
构建 wasm
运行打包:
yarn asbuild
之后就会在 ./build
下得到优化后和优化前的 .wasm
二进制文件:
实际调用
在 ./test/index.js
内编写:
const loader = require("@assemblyscript/loader");
const fs =require('fs')
const text = '年轻人不讲武德'
loader.instantiate(
fs.readFileSync('../build/optimized.wasm'),
{env: { memory: new WebAssembly.Memory({initial:10, maximum:100})} }
).then(({ exports }) => {
console.time("测试 wasm 速度: ")
const { __getString, __retain, __newString, __release } = exports
const { getBase64, Base64 } = exports
const Base64Ptr = getBase64()
const base64 = Base64.wrap(Base64Ptr)
for(let i = 0;i<10000;i++) {
const textPtr = __retain(__newString(text))
const outputPtr = base64.encode(textPtr)
// console.log(__getString(outputPtr))
__release(textPtr)
__release(outputPtr)
}
__release(Base64Ptr)
console.timeEnd("测试 wasm 速度: ")
})
下面我们分块讲解:
loader.instantiate(
fs.readFileSync('../build/optimized.wasm'),
{env: { memory: new WebAssembly.Memory({initial:10, maximum:100})} }
)
作用:初始化一个微型加载器 loader ,他内置了很多预设,可以帮我们省去很多配置的功夫,往往我们只需要指定 env.memory
参数定义初始内存值即可(在这里不指定也可),当你需要更大内存运算时,记得指定。
参数1:这里第一个参数是读入 .wasm
文件,可以是 fs
本地读取,也可以是 fetch
远程拉取(在浏览器的情况)。
参数2:loader 的配置,正常情况可以不传,采用默认配置,更多配置详见 node_modules/@assemblyscript/loader/index.d.ts
中的 Imports
与官网说明。
.then(({ exports }) => {
console.time("测试 wasm 速度: ")
// ...
console.timeEnd("测试 wasm 速度: ")
})
拿到异步加载的结果并结构得到 exports
,即为我们在 ./assembly/index.ts
导出的函数。
注:实际上 exports
并不只有我们导出的函数,他还有一些“辅助”函数,帮助我们更友好的使用 WebAssembly 。
// 导出辅助函数
const { __getString, __retain, __newString, __release } = exports
// 导出我们编写的函数
const { getBase64, Base64 } = exports
// 获取 class 单例的指针
const Base64Ptr = getBase64()
// 将指针转为实际的 class 类
const base64 = Base64.wrap(Base64Ptr)
在这里,我们先将辅助函数导出,为什么需要辅助函数?因为在 WebAssembly 中,不存在字符串和 class 等基本类型的概念,一切均为 buffer 与 number ,所以辅助函数的作用就是帮我们做了 string 和 class 的中间转化处理,真是非常友好了!
name | description |
---|---|
__getString | 从一个 string 的指针获取实际的字符串值 |
__retain | 定义一个引用 id ,以便后续回收,多次调用的 id 会进行累加(并不需要我们维护) |
__newString | 将实际字符串转为 string 的指针,以便传入 |
__release | 根据 __retain 释放引用 |
更多辅助函数请看官网说明:loader usage
// 执行 10000 次编码
for(let i = 0;i<10000;i++) {
// 获取一个 string 的指针
const textPtr = __retain(__newString(text))
// 传入指针得到返回值 string 的指针
const outputPtr = base64.encode(textPtr)
// 可以通过 __getString 方法将 string 指针转为实际的字符串
// console.log(__getString(outputPtr))
// 清理引用
__release(textPtr)
__release(outputPtr)
}
__release(Base64Ptr)
我们对其做 10000
次编码,实际中被编码的 text
会发生变化,可能时间会更长:
校验
我们打印一次出来看一下结果:
校验:
成功!
对比
下面我们用相同的代码进行测试直接使用的速度:
const { getBase64 } = require('./encode')
console.time("测试直接使用速度")
const base = getBase64()
for(let i = 0;i<10000;i++) {
base.encode(text)
// console.log(base.encode(text))
}
console.timeEnd("测试直接使用速度")
结果:
总结
目前 WebAssembly 主要是应用在音视频处理和网页游戏上,可以借助相关库的便利性,比如 ffmpeg 实现转码,音视频压缩,B 站视频上传过程即可选择封面等。
加上规范的不成熟,旧版本浏览器兼容问题,以及如此缓慢的速度,虽然不能否定,但也请不要太吹嘘 WebAssembly 了。