前端展望-初识Wasm(WebAssembly)

Wasm(WebAssembly)是什么?

Wasm/WebAssembly是一个可移植、体积小、加载快并且兼容 Web 的全新格式,基于栈式虚拟机的二进制指令集,可以作为编程语言的编译目标,能够部署在 web 客户端和服务端的应用中。

Wasm(WebAssembly)的由来

在业务需求越来越复杂的今天,前端的开发逻辑越来越复杂,代码量也随之变的越来越多,项目越来越大,复杂度也越来越高,在性能不好的电脑上,一些复杂的运算或者图形处理需要耗费用户几十秒甚至更长的时间。

于是乎,WebAssembly横空出世,作为一种低级的类汇编语言,相比JavaScript,省去了各种高级语言向低级语言的中间转换操作,代码可以得到更快更高效的执行,配合web worker等技术可以有效应对高复杂度运算或图形处理所带来的性能损耗。

Wasm(WebAssembly)解决了什么问题?

  • 优化JavaScript 的性能瓶颈问题,比如各种高复杂度运算或图形处理等等。
  • 让其他语言编写的代码在浏览器端复用成为可能,比如AutoCAD等桌面软件向web端的迁移。
  • 让其他语言编写的成熟的库在浏览器端可以使用,比如ffpmeg音视频类处理库,ImageMagick图形处理类库,各种AI库等等。

Wasm(WebAssembly)浏览器兼容性

可以看到WebAssembly目前的支持情况已经算是比较高了。

案例实践

在了解了一些WebAssembly的基础知识后,我们通过一个案例来实践WebAssembly的使用。

  • 环境准备:

在开始着手编码之前,我们先准备好后续过程中需要的环境,这里我们准备选用C语言来编写WebAssembly应用。

①、C语言编码环境准备:

这个就不多说了,我们常用的编辑器都可以编写C代码。

②、C语言到wasm编译环境准备:

这里我们选用emscripten(C和C++编译到 Wasm的专用工具),以ubuntu为例演示安装过程,window环境安装参考链接:https://www.jianshu.com/p/bdae7356773c

首先我们切换到一个不会被删除的目录中,然后在当前目录的命令行中依次输入以下命令:

git clone https://github.com/emscripten-core/emsdk.git 
cd emsdk
git pull
./emsdk install latest
./emsdk activate latest
source ./emsdk_env.sh // 设置环境变量

然后输入命令 emcc 验证安装,只要提示不是“ Command 'emcc' not found ”则说明安装成功了。

注意:每次新开命令行使用 emcc 命令前,需要切换到emsdk存储目录重新设置环境变量: source ./emsdk_env.sh 

  • 开始编码:

这里我们编写一个计算使用C语言编写一个计算阶乘的函数:

double factorial(int n) {
    if (n <= 1) {
        return 1;
    }
    return factorial(n - 1) * n;
}
  • 编译Wasm

在emsdk环境变量设置完毕后,切换到我们存放代码的目录,并执行以下命令:

emcc factorial.c -O3 -s WASM=1 -s SIDE_MODULE=1 -o factorial.wasm

其中,emcc 就是Emscripten 编译器,factorial.c 是我们需要编译的C文件,-O3 表示这次编译需要优化,-s WASM=1 表示输出wasm的文件,-s SIDE_MODULE=1 表示就只要这一个模块,不要给我其他乱七八糟的代码; -o test.wasm 是我们的输出文件,emcc命令其他用法可参考:https://blog.csdn.net/wngzhem/article/details/105192706

  • 在node中调用wasm
    const fs = require('fs');
    const args = process.argv.slice(2);
    const src = new Uint8Array(fs.readFileSync('./factorial.wasm'));
    
    const env = {
        memoryBase: 0,
        tableBase: 0,
        memory: new WebAssembly.Memory({
            initial: 256
        }),
        table: new WebAssembly.Table({
            initial: 2,
            element: 'anyfunc'
        }),
        abort: () => { throw 'abort'; }
    };
    WebAssembly
        .instantiate(src, {env: env})
        .then(ret => {
            const { factorial } = ret.instance.exports;
            if (factorial) {
                console.log(factorial(Number(args[0])))
            }
        })
        .catch(e => console.log(e));
    

    将上面的代码保存为 node_test.js 并执行 node node_test.js 数字 ,运行结果如下:

    • 在浏览器中调用wasm
    <!DOCTYPE html>
    <html lang="en">
      <head>
        <meta charset="UTF-8" />
        <meta http-equiv="X-UA-Compatible" content="IE=edge" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>factorial数列</title>
      </head>
      <body>
        <p><label for="num">请输入一个数字:</label><input type="number" id="num" /></p>
        <p>结果:<span id="ret"></span></p>
        <script type="text/javascript">
            const getExports = getCachedExports();
            
            num.onchange = e => {
                const val = Number(e.target.value);
                factorial_wasm(val)
                    .then(val => ret.innerText = val)
            }
    
            function factorial_wasm(n) {
                return getExports('./factorial.wasm')
                    .then(exports => exports.factorial)
                    .then(func => func(n));
            }
    
            function getCachedExports(url) {
                let cacheExports = null;
                return async (url) => {
                if (cacheExports) {
                    return cacheExports;
                }
                const env = {
                    memoryBase: 0,
                    tableBase: 0,
                    memory: new WebAssembly.Memory({
                    initial: 256,
                    }),
                    table: new WebAssembly.Table({
                    initial: 2,
                    element: 'anyfunc',
                    }),
                };
                const exports = await fetch(url)
                    .then((response) => {
                    return response.arrayBuffer();
                    })
                    .then((bytes) => {
                    return WebAssembly.instantiate(bytes, { env: env });
                    })
                    .then((ret) => {
                    return ret.instance.exports;
                    })
                    .catch((err) => {
                    throw new Error(err);
                    });
                cacheExports = exports;
                return exports;
                };
            }
    </script>
      </body>
    </html>
    

    将以上代码保存为 test.html , 并将其放至本地服务器或在线服务器中访问即可。

    思考扩展

    上面我们提到了wasm可以帮助解决JavaScript的性能瓶颈问题,那么我们就以计算阶乘为例比对一下使用js的方式和使用wasm的方式分别的耗时。这里我们使用两种js的方式计算阶乘:

    ①、递归的方式

    function factorial_normal(n) {
        if (n <= 1) {
            return 1;
        }
        return factorial_normal(n - 1) * n;
    }
    

    ②、动态规划的方式

    function factorial_dp(n) {
        const dp = [];
        dp[0] = dp[1] = 1;
        for (let i=2; i<=n; i++) {
            dp[i] = i * dp[i-1];
        }
        return dp[n];
    }
    

    然后我们编写一个函数用于监测函数的执行耗时:

    async function monitor(func, ...args) {
        console.time(func.name);
        await func(...args);
        console.timeEnd(func.name);
    }
    

    运行效果:

    忽略第一次因为wasm初始化的耗时,可以看到随着输入数据的变大,使用wasm计算的优势就凸显出来了。

    如需下载本文所涉及的源码,请在关注本站公众号后发送:factorial

    • 支付宝二维码 支付宝
    • 微信二维码 微信

    本文地址: /wasm.html

    版权声明: 本文为原创文章,版权归 逐梦个人博客 所有,欢迎分享本文,转载请保留出处!

    相关文章