美文网首页
WebAssembly调研

WebAssembly调研

作者: 郝同学1208 | 来源:发表于2022-03-24 20:46 被阅读0次

    文章序

    之前在浏览前端技术的时候留意到了webAssembly这项技术,看到它是类似于在前端调用后端语言的源码或者说功能,再加上笔者浅学过C++和java,故在此做一些调研和尝试

    什么是WebAssembly

    WebAssembly以下简称WASM,通过将传统意义上的后端语言(C、C++、Java、Rust等)编译成字节码,.wasm格式文件,在浏览器上调用解释器编译成机器码才能运行,因此WASM并不能真正达到汇编语言级别的性能,与Java比较像,都是编译成中间字节码,然后交由解释器工作(在Java中则是JVM)。

    2015年4月,WebAssembly Community Group 成立;
    2015年6月,WebAssembly第一次以WCG的官方名义向外界公布;
    2016年8月,WebAssembly开始进入了漫长的“Browser Preview”阶段;
    2017年2月,WebAssembly官方LOGO在Github上的众多讨论中被最终确定;同年同月,一个历史性的阶段,四大浏览器(FireFox、Chrome、Edge、WebKit)在WebAssembly的MVP(最小可用版本)标准实现上达成共识,这意味着WebAssembly在其MVP标准上的“Brower Preview”阶段已经结束;
    2017年8月,W3C WebAssembly Working Group 成立,意味着WebAssembly正式成为W3C众多技术标准中的一员。
    2019年12月,WebAssembly成为万维网联盟(W3C)的推荐标准,与HTML,CSS和JavaScript一起成为Web的第四种语言。

    使用方法

    从.wasm源文件到实例化的对象主要有三个步骤,加载->编译->实例化->调用。
    加载:读取.wasm字节码到本地中,一般是通过请求从网络中取得。
    编译:在Worker线程进行,编译成平台相关的代码。
    实例化:将宿主环境的一些对象、方法导入到wasm模块中,比如导入操作dom的方法。
    调用:通过上一步已经实例化的对象,来调用wasm模块中的方法。主要有两种类型的API,一种是js提供的api,另一种是Web提供的api,Web提供的api支持流式编译实例化。
    js的方法,WebAssembly.instantiate(bufferSource,importObject),可以完成编译和实例化。bufferSource是含有效Wasm模块二进制字节码的ArrayBuffer或TypedArray对象。importObject是要导入到Wasm模块中的对象。方法在调用后返回一个Promise对象,resolve后返回一个对象,该对象包含编译好的module和已经实例化的instance,模块导出的方法可以通过instance对象进行调用。
    web的方法,WebAssembly.instantiateStreaming(source,importObject)。不同之处在于第一个参数,这里的source指的是尚未Resolve的Response对象(window.fetch调用后会返回该对象),好处就是可以边读取.wasm字节流,边进行编译。其他参数和返回值和js的api均一致。

    jsAPI尝试

    先简单的尝试一下,我们直接构造一个wasm模块的TypedArray对象,该模块包含了一个add方法,然后调用WebAssembly.instantiate进行编译和实例化。对应的C++代码。

    #include <emscripten.h>
    
    extern "C" {
      EMSCRIPTEN_KEEPALIVE 
      int add(int a, int b) {
        return a + b;
      }
    }
    
    对应的.wasm字节码:
    00 61 73 6D 01 00 00 00 01 17 05 60 00 01 7F 60
    00 00 60 01 7F 00 60 01 7F 01 7F 60 02 7F 7F 01
    7F 03 07 06 01 04 00 02 03 00 04 05 01 70 01 02
    02 05 06 01 01 80 02 80 02 06 0F 02 7F 01 41 90
    88 C0 02 0B 7F 00 41 84 08 0B 07 82 01 09 06 6D
    65 6D 6F 72 79 02 00 19 5F 5F 69 6E 64 69 72 65
    63 74 5F 66 75 6E 63 74 69 6F 6E 5F 74 61 62 6C
    65 01 00 03 61 64 64 00 01 0B 5F 69 6E 69 74 69
    61 6C 69 7A 65 00 00 10 5F 5F 65 72 72 6E 6F 5F
    6C 6F 63 61 74 69 6F 6E 00 05 09 73 74 61 63 6B
    53 61 76 65 00 02 0C 73 74 61 63 6B 52 65 73 74
    6F 72 65 00 03 0A 73 74 61 63 6B 41 6C 6C 6F 63
    00 04 0A 5F 5F 64 61 74 61 5F 65 6E 64 03 01 09
    07 01 00 41 01 0B 01 00 0A 30 06 03 00 01 0B 07
    00 20 00 20 01 6A 0B 04 00 23 00 0B 06 00 20 00
    24 00 0B 10 00 23 00 20 00 6B 41 70 71 22 00 24
    00 20 00 0B 05 00 41 80 08 0B
    
    然后直接在控制台输入下边的代码:
    WebAssembly.instantiate(new Uint8Array(`
      00 61 73 6D 01 00 00 00 01 17 05 60 00 01 7F 60
    00 00 60 01 7F 00 60 01 7F 01 7F 60 02 7F 7F 01
    7F 03 07 06 01 04 00 02 03 00 04 05 01 70 01 02
    02 05 06 01 01 80 02 80 02 06 0F 02 7F 01 41 90
    88 C0 02 0B 7F 00 41 84 08 0B 07 82 01 09 06 6D
    65 6D 6F 72 79 02 00 19 5F 5F 69 6E 64 69 72 65
    63 74 5F 66 75 6E 63 74 69 6F 6E 5F 74 61 62 6C
    65 01 00 03 61 64 64 00 01 0B 5F 69 6E 69 74 69
    61 6C 69 7A 65 00 00 10 5F 5F 65 72 72 6E 6F 5F
    6C 6F 63 61 74 69 6F 6E 00 05 09 73 74 61 63 6B
    53 61 76 65 00 02 0C 73 74 61 63 6B 52 65 73 74
    6F 72 65 00 03 0A 73 74 61 63 6B 41 6C 6C 6F 63
    00 04 0A 5F 5F 64 61 74 61 5F 65 6E 64 03 01 09
    07 01 00 41 01 0B 01 00 0A 30 06 03 00 01 0B 07
    00 20 00 20 01 6A 0B 04 00 23 00 0B 06 00 20 00
    24 00 0B 10 00 23 00 20 00 6B 41 70 71 22 00 24
    00 20 00 0B 05 00 41 80 08 0B`.trim().split(/[\s\r\n]+/g).map(str => parseInt(str, 16))
    )).then(({instance}) => {
      const { add } = instance.exports
      console.log('2 + 4 =', add(2, 4))
    })
    
    输出2 + 4 = 6
    

    WebAPI尝试

    我们再尝试一下流式编译。直接使用之前的斐波纳契数字的fibonacci.wasm模块。首先我们需要提供一个简单的HTTP服务,用来返回.wasm文件。新建一个node.js文件。

    const http = require('http');
    const url = require('url');
    const fs = require('fs');
    const path = require('path');
    
    const PORT = 8888;  // 服务器监听的端口号;
    
    const mime = {
      "html": "text/html;charset=UTF-8",
      "wasm": "application/wasm"  //当遇到对".wasm"格式文件的请求时,返回特定的MIME头;
    };
    
    http.createServer((req, res) => {
      let realPath = path.join(__dirname, `.${url.parse(req.url).pathname}`);
      //检查所访问文件是否存在,且是否可读;
      fs.access(realPath, fs.constants.R_OK, err => {  
        if (err) {
          res.writeHead(404, { 'Content-Type': 'text/plain' });
          res.end();
        } else {
          fs.readFile(realPath, "binary", (err, file) => {
            if (err) {
              //文件读取失败时返回500;
              res.writeHead(500, { 'Content-Type': 'text/plain' });
              res.end();
            } else {
              //根据请求的文件返回相应的文件内容;
              let ext = path.extname(realPath);
              ext = ext ? ext.slice(1) : 'unknown';
              let contentType = mime[ext] || "text/plain";
              res.writeHead(200, { 'Content-Type': contentType });
              res.write(file, "binary");
              res.end();
            }
          });
        }
      });
    }).listen(PORT);
    console.log("Server is runing at port: " + PORT + ".");
    

    然后来编写我们的js部分,讲到斐波那契数字,我们顺便做一个性能的测试,来比较一下使用wasm的方式和原生js的求解速度。

    function fibonacciJS(n) {
      if (n < 2) {
        return 1;
      }
      return fibonacciJS(n - 1) + fibonacciJS(n - 2);
    }
    const response = fetch("fibonacci.wasm");
    const num = [5, 15, 25, 35, 45];
    WebAssembly.instantiateStreaming(response).then(
      ({ instance }) => {
        let { fibonacci } = instance.exports;
        for(let n of num) {
          console.log(`斐波纳切数字: ${n},运行 10 次`)
          let cTime = 0;
          let jsTime = 0;
          for(let time = 0; time < 10; time++) {
            let start = performance.now();
            fibonacci(n)
            cTime += (performance.now() - start)
    
            start = performance.now();
            fibonacciJS(n)
            jsTime += (performance.now() - start)
          }
          console.log(`wasm 模块平均调用时间:${cTime / 10}ms`)
          console.log(`js 模块平均调用时间:${jsTime / 10}ms`)
        }
      }
    )
    

    然后执行node node.js开启http服务,接着在浏览器中打开http://localhost:8888/index.html,控制台中输出如下:

    斐波纳切数字: 5,运行 10 次
    index.html:34 wasm 模块平均调用时间:0.001499993959441781ms
    index.html:35 js 模块平均调用时间:0.005500001134350896ms
    index.html:22 斐波纳切数字: 15,运行 10 次
    index.html:34 wasm 模块平均调用时间:0.005999993300065398ms
    index.html:35 js 模块平均调用时间:0.15650001005269587ms
    index.html:22 斐波纳切数字: 25,运行 10 次
    index.html:34 wasm 模块平均调用时间:0.6239999900572002ms
    index.html:35 js 模块平均调用时间:1.1620000121183693ms
    index.html:22 斐波纳切数字: 35,运行 10 次
    index.html:34 wasm 模块平均调用时间:70.59700000681914ms
    index.html:35 js 模块平均调用时间:126.21099999523722ms
    index.html:22 斐波纳切数字: 45,运行 10 次
    index.html:34 wasm 模块平均调用时间:8129.7520000021905ms
    index.html:35 js 模块平均调用时间:16918.658500007587ms
    

    可以看到wasm很明显的提高了运行速度,运行时间稳定在js的一半,当规模达到45的时候,wasm的运行时间比js少了整整8秒。
    这里也可以看出,如果对于计算密集型的应用,wasm可以大展身手了,因此WASM适合运用于游戏、视频处理、AR等方面。

    不止于WebWasm

    除了应用在浏览器中,也可以应用到out-of-web环境中。通过WASI(WebAssembly System Interface,Wasm操作系统接口)标准,Wasm可以直接与操作系统打交道。通过已经在各种环境实现了WASI标准的虚拟机,我们就可以将wasm用在嵌入式、IOT物联网以及甚至云,AI和区块链等特殊的领域和场景中。
    有了WASI标准,文章最开始介绍的当前应用的架构在未来可能会发生质的改变。
    上边架构的最大问题就是各个操作系统不能兼容,同一个app需要采用不同的语言在不同平台下各实现一次。比如一款A应用,如果想实现跨平台的话,我们需要用java完成在安卓上的开发,用Objective-C实现iOS上的开发,用C#实现PC端的开发...但如果有了wasm,我们只需要选择任意一门语言,然后编译成wasm,就可以分发到各个平台上了。

    总结

    目前来讲WASM在浏览器性能上和JS能打个55开,互有胜负,WASM有他擅长的地方,JS有JS优势的地方,我认为WASM并不会替代JS/TS,WASM的统一和整合,正如下图

    将所有的开发语言和所有的运行环境连接起来,只不过因为web天生的多平台性,目前在web端应用比较成熟,而这也确实在某些业务上能够让前端直接去调用C++的相关代码。
    但是就目前来讲,你指望一个前端工程师为了性能优化的原因,去学习C++然后再编写C++代码编译成.wasm二进制文件再放到浏览器环境上运行?不可能的,所以目前WASM的应用主要还是在JS没有相应合适的三方库的情况下,去借用C++等后端语言的三方库,比如视频处理中的ffmpeg;或者将一部分后端的逻辑处理放到前端,减少服务压力,不过这也是很没必要。

    我认为,WASM目前阶段已经具备了很多成熟的应用,而且未来可期,作为未来的技术方向有必要了解一下。

    相关文章

      网友评论

          本文标题:WebAssembly调研

          本文链接:https://www.haomeiwen.com/subject/orrltrtx.html