WebAssembly 初體驗(yàn):從零開(kāi)始重構(gòu)計(jì)算模塊
WebAssembly 的概念、意義以及未來(lái)帶來(lái)的性能提升相信已是耳熟能詳,筆者在前端每周清單系列中也是經(jīng)常會(huì)推薦 WebAssembly 相關(guān)文章。不過(guò)筆者也只是了解其概念而未真正付諸實(shí)踐,本文即是筆者在將我司某個(gè)簡(jiǎn)單項(xiàng)目中的計(jì)算模塊重構(gòu)為 WebAssembly 過(guò)程中的總結(jié)。在簡(jiǎn)單的實(shí)踐中筆者個(gè)人感覺(jué),WebAssembly 的抽象程度會(huì)比 JavaScript 高不少,未來(lái)對(duì)于大型項(xiàng)目的遷移,對(duì)于純前端工程師而言可能存在的坑也是不少,仿佛又回到了被指針統(tǒng)治的年代。本文筆者使用的案例已經(jīng)集成到了 React 腳手架 create-react-boilerplate 中 ,可以方便大家快速本地實(shí)踐。
編譯環(huán)境搭建
我們使用 Emscripten 將 C 代碼編譯為 wasm 格式,官方推薦的方式是首先下載 Portable Emscripten SDK for Linux and OS X (emsdk-portable.tar.gz) 然后利用 emsdk 進(jìn)行安裝:
- $ ./emsdk update
- $ ./emsdk install latest
- # 如果出現(xiàn)異常使用 ./emsdk install sdk-1.37.12-64bit
- # https://github.com/kripken/emscripten/issues/5272
安裝完畢后激活響應(yīng)環(huán)境即可以進(jìn)行編譯:
- $ ./emsdk activate latest
- $ source ./emsdk_env.sh # you can add this line to your .bashrc
筆者在本地執(zhí)行上述搭建步驟時(shí)一直失敗,因此改用了 Docker 預(yù)先配置好的鏡像進(jìn)行處理:
- # 拉取 Docker 鏡像
- docker pull 42ua/emsdk
- # 執(zhí)行編譯操作
- docker run --rm -v $(pwd):/home/src 42ua/emsdk emcc hello_world.c
對(duì)應(yīng)的 Dockfile 如下所示,我們可以自行修改以適應(yīng)未來(lái)的編譯環(huán)境:
- FROM ubuntu
- RUN \
- apt-get update && apt-get install -y build-essential \
- cmake python2.7 python nodejs-legacy default-jre git-core curl && \
- apt-get clean && \
- \
- cd ~/ && \
- curl -sL https://s3.amazonaws.com/mozilla-games/emscripten/releases/emsdk-portable.tar.gz | tar xz && \
- cd emsdk-portable/ && \
- ./emsdk update && \
- ./emsdk install -j1 latest && \
- ./emsdk activate latest && \
- \
- rm -rf ~/emsdk-portable/clang/tag-*/src && \
- find . -name "*.o" -exec rm {} \; && \
- find . -name "*.a" -exec rm {} \; && \
- find . -name "*.tmp" -exec rm {} \; && \
- find . -type d -name ".git" -prune -exec rm -rf {} \; && \
- \
- apt-get -y --purge remove curl git-core cmake && \
- apt-get -y autoremove && apt-get clean
- # http://docs.docker.com/engine/reference/run/#workdir
- WORKDIR /home/src
到這里基本環(huán)境已經(jīng)配置完畢,我們可以對(duì)簡(jiǎn)單的 counter.c 進(jìn)行編譯,源文件如下:
- int counter = 100;
- int count() {
- counter += 1;
- return counter;
- }
編譯命令如下所示,如果本地安裝好了 emcc 則可以直接使用,否則使用 Docker 環(huán)境進(jìn)行編譯:
- $ docker run --rm -v $(pwd):/home/src 42ua/emsdk emcc counter.c -s WASM=1 -s SIDE_MODULE=1 -o counter.wasm
- $ emcc counter.c -s WASM=1 -s SIDE_MODULE=1 -o counter.wasm
- # 如果出現(xiàn)以下錯(cuò)誤,則是由如下參數(shù)
- # WebAssembly Link Error: import object field 'DYNAMICTOP_PTR' is not a Number
- emcc counter.c -O1 -s WASM=1 -s SIDE_MODULE=1 -o counter.wasm
這樣我們就得到了 WebAssembly 代碼:
與 JavaScript 集成使用
獨(dú)立的 .wasm 文件并不能直接使用,我們需要在客戶(hù)端中使用 JavaScript 代碼將其加載進(jìn)來(lái)。最樸素的加載 WebAssembly 的方式就是使用 fetch 抓取然后編譯,整個(gè)過(guò)程可以封裝為如下函數(shù):
- // 判斷是否支持 WebAssembly
- if (!('WebAssembly' in window)) {
- alert('當(dāng)前瀏覽器不支持 WebAssembly!');
- }
- // Loads a WebAssembly dynamic library, returns a promise.
- // imports is an optional imports object
- function loadWebAssembly(filename, imports) {
- // Fetch the file and compile it
- return fetch(filename)
- .then(response => response.arrayBuffer())
- .then(buffer => WebAssembly.compile(buffer))
- .then(module => {
- // Create the imports for the module, including the
- // standard dynamic library imports
- imports = imports || {};
- imports.env = imports.env || {};
- imports.env.memoryBase = imports.env.memoryBase || 0;
- imports.env.tableBase = imports.env.tableBase || 0;
- if (!imports.env.memory) {
- imports.env.memory = new WebAssembly.Memory({ initial: 256 });
- }
- if (!imports.env.table) {
- imports.env.table = new WebAssembly.Table({ initial: 0, element: 'anyfunc' });
- }
- // Create the instance.
- return new WebAssembly.Instance(module, imports);
- });
- }
我們可以使用上述工具函數(shù)加載 wasm 文件:
- loadWebAssembly('counter.wasm')
- .then(instance => {
- var exports = instance.exports; // the exports of that instance
- var count = exports. _count; // the "_count" function (note "_" prefix)
- // 下面即可以調(diào)用 count 函數(shù)
- }
- );
而在筆者的腳手架中,使用了 wasm-loader 進(jìn)行加載,這樣可以將 wasm 直接打包在 Bundle 中,然后通過(guò) import 導(dǎo)入:
- import React, { PureComponent } from "react";
- import CounterWASM from "./counter.wasm";
- import Button from "antd/es/button/button";
- import "./Counter.scss";
- /**
- * Description 簡(jiǎn)單計(jì)數(shù)器示例
- */
- export default class Counter extends PureComponent {
- state = {
- count: 0
- };
- componentDidMount() {
- this.counter = new CounterWASM({
- env: {
- memoryBase: 0,
- tableBase: 0,
- memory: new window.WebAssembly.Memory({ initial: 256 }),
- table: new window.WebAssembly.Table({ initial: 0, element: "anyfunc" })
- }
- });
- this.setState({
- count: this.counter.exports._count()
- });
- }
- /**
- * Description 默認(rèn)渲染函數(shù)
- */
- render() {
- const isWASMSupport = "WebAssembly" in window;
- if (!isWASMSupport) {
- return (
- <div>
- 瀏覽器不支持 WASM
- </div>
- );
- }
- return (
- <div className="Counter__container">
- <span>
- 簡(jiǎn)單計(jì)數(shù)器示例:
- </span>
- <span>{this.state.count}</span>
- <Button
- type="primary"
- onClick={() => {
- this.setState({
- count: this.counter.exports._count()
- });
- }}
- >
- 點(diǎn)擊自增
- </Button>
- </div>
- );
- }
- }
在使用 wasm-loader 時(shí),其會(huì)調(diào)用 new WebAssembly.Instance(module, importObject);
module 即 WebAssembly.Module 實(shí)例。
importObject 即默認(rèn)的由 wasm-loader 提供的對(duì)象。
簡(jiǎn)單游戲引擎重構(gòu)
上文我們討論了利用 WebAssembly 重構(gòu)簡(jiǎn)單的計(jì)數(shù)器模塊,這里我們以簡(jiǎn)單的游戲?yàn)槔换ナ降母惺?WebAssembly 帶來(lái)的性能提升,可以直接查看游戲的在線演示。這里的游戲引擎即是執(zhí)行部分計(jì)算與重新賦值操作,譬如這里的計(jì)算下一個(gè)位置狀態(tài)的函數(shù)在 C 中實(shí)現(xiàn)為:
- EMSCRIPTEN_KEEPALIVE
- void computeNextState()
- {
- loopCurrentState();
- int neighbors = 0;
- int i_m1, i_p1, i_;
- int j_m1, j_p1;
- int height_limit = height - 1;
- int width_limit = width - 1;
- for (int i = 1; i < height_limit; i++)
- {
- i_m1 = (i - 1) * width;
- i_p1 = (i + 1) * width;
- i_ = i * width;
- for (int j = 1; j < width_limit; j++)
- {
- j_m1 = j - 1;
- j_p1 = j + 1;
- neighbors = current[i_m1 + j_m1];
- neighbors += current[i_m1 + j];
- neighbors += current[i_m1 + j_p1];
- neighbors += current[i_ + j_m1];
- neighbors += current[i_ + j_p1];
- neighbors += current[i_p1 + j_m1];
- neighbors += current[i_p1 + j];
- neighbors += current[i_p1 + j_p1];
- if (neighbors == 3)
- {
- next[i_ + j] = 1;
- }
- else if (neighbors == 2)
- {
- next[i_ + j] = current[i_ + j];
- }
- else
- {
- next[i_ + j] = 0;
- }
- }
- }
- memcpy(current, next, width * height);
- }
而對(duì)應(yīng)的 JS 版本引擎的實(shí)現(xiàn)為:
- computeNextState() {
- let neighbors, iM1, iP1, i_, jM1, jP1;
- this.loopCurrentState();
- for (let i = 1; i < this._height - 1; i++) {
- iM1 = (i - 1) * this._width;
- iP1 = (i + 1) * this._width;
- i_ = i * this._width;
- for (let j = 1; j < this._width - 1; j++) {
- jM1 = j - 1;
- jP1 = j + 1;
- neighbors = this._current[iM1 + jM1];
- neighbors += this._current[iM1 + j];
- neighbors += this._current[iM1 + jP1];
- neighbors += this._current[i_ + jM1];
- neighbors += this._current[i_ + jP1];
- neighbors += this._current[iP1 + jM1];
- neighbors += this._current[iP1 + j];
- neighbors += this._current[iP1 + jP1];
- if (neighbors === 3) {
- this._next[i_ + j] = 1;
- } else if (neighbors === 2) {
- this._next[i_ + j] = this._current[i_ + j];
- } else {
- this._next[i_ + j] = 0;
- }
- }
- }
- this._current.set(this._next);
- }
本部分的編譯依舊是直接將 [engine.c]() 編譯為 engine.wasm,不過(guò)在導(dǎo)入的時(shí)候我們需要?jiǎng)討B(tài)地向 wasm 中注入外部函數(shù):
- this.module = new EngineWASM({
- env: {
- memoryBase: 0,
- tableBase: 0,
- memory: new window.WebAssembly.Memory({ initial: 1024 }),
- table: new window.WebAssembly.Table({ initial: 0, element: "anyfunc" }),
- _malloc: size => {
- let buffer = new ArrayBuffer(size);
- return new Uint8Array(buffer);
- },
- _memcpy: (source, target, size) => {
- let sourceEnd = source.byteLength;
- let i, j;
- for (
- (i = 0), (j = 0), (k = new Uint8Array(target)), (l = new Uint8Array(
- source
- ));
- i < sourceEnd;
- ++i, ++j
- )
- k[j] = l[i];
- }
- }
- });
到這里文本告一段落,筆者***需要聲明的是因?yàn)檫@只是隨手做的實(shí)驗(yàn),***的代碼包括對(duì)于內(nèi)存的操作可能存在潛在問(wèn)題,請(qǐng)讀者批評(píng)指正。
【本文是51CTO專(zhuān)欄作者“張梓雄 ”的原創(chuàng)文章,如需轉(zhuǎn)載請(qǐng)通過(guò)51CTO與作者聯(lián)系】