当前位置: 首页 > 科技观察

WebAssembly初体验:从零开始重构计算模块

时间:2023-03-16 22:05:49 科技观察

WebAssembly的概念、意义和未来的性能提升相信大家都不陌生。作者也经常在前端周榜系列推荐WebAssembly相关文章。但是,作者只是理解了它的概念,并没有真正付诸实践。本文是作者在将我们公司一个简单项目中的一个计算模块重构为WebAssembly过程中的总结。在简单的实践中,笔者个人感觉WebAssembly的抽象层次会比JavaScript高很多。对于以后大型项目的迁移,对于纯前端工程师来说可能会有很多坑,仿佛回到了指针统治的时代。本文作者使用的案例已经集成到React脚手架create-react-boilerplate中,方便大家在本地快速实践。搭建编译环境我们使用Emscripten将C代码编译成wasm格式。官方推荐的方式是先下载PortableEmscriptenSDKforLinuxandOSX(emsdk-portable.tar.gz)然后使用emsdk安装:$./emsdkupdate$。/emsdkinstalllatest#如果有异常,使用./emsdkinstallsdk-1.37.12-64bit#https://github.com/kripken/emscripten/issues/5272安装后激活响应环境,编译:$./emsdkactivalatest$来源。/emsdk_env.sh#youcanaddthislinetoyour.bashrc笔者在本地一直无法执行上述构建步骤,所以转而使用Docker预配置镜像进行处理:#PulltheDockerimagedockerpull42ua/emsdk#Executethecompilationoperationdockerrun--rm-v$(pwd):/home/src42ua/emsdkemcchello_world.c对应的Dockfile如下,我们可以修改它以适应以后的编译环境:FROMubuntuRUN\apt-getupdate&&apt-getinstall-ybuild-essential\cmakepython2。7pythonnodejs-legacydefault-jregit-corecurl&&\apt-getclean&&\\cd~/&&\curl-sLhttps://s3.amazonaws.com/mozilla-games/emscripten/releases/emsdk-portable.tar.gz|tarxz&&\cdemsdk-便携的/&&\。/emsdkupdate&&\./emsdkinstall-j1latest&&\./emsdkactivatelatest&&\\rm-rf~/emsdk-portable/clang/tag-*/src&&\find.-name"*.o"-execrm{}\;&&\find.-name"*.a"-execrm{}\;&&\find.-name"*.tmp"-execrm{}\;&&\find.-typed-name".git"-prune-execrm-rf{}\;&&\\apt-get-y--purgeremovecurlgit-corecmake&&\apt-get-yautoremove&&apt-getclean#http//docs.docker.com/engine/reference/run/#workdirWORKDIR/home/src到这里基本环境已经配置好了,我们可以编译一个简单的counter.c,源文件如下:intcounter=100;intcount(){counter+=1;returncounter;}编译命令如下,如果你本地安装了emcc,可以直接使用,否则使用Docker环境编译:$dockerrun--rm-v$(pwd):/home/src42ua/emsdkemcccounter.c-sWASM=1-sSIDE_MODULE=1-ocounter.wasm$emcccounter.c-sWASM=1-sSIDE_MODULE=1-ocounter.wasm#如果出现如下错误,是以下参数引起的#WebAssemblyLinkError:importobjectfield'DYNAMICTOP_PTR'isnotaNumberemcccounter.c-O1-sWASM=1-sSIDE_MODULE=1-ocounter.wasm这样我们就得到了WebAssembly代码:IntegrationwithJavaScript使用独立的.wasm文件是不能直接使用的,我们需要在客户端用JavaScript代码加载进来加载WebAssembly最简单的方式就是使用fetch抓取然后编译。整个过程可以封装为如下函数://判断是否支持WebAssemblyif(!('WebAssembly'inwindow)){alert('当前浏览器不支持WebAssembly!');}//加载WebAssembly动态库,返回一个promise.//importsisanoptionalimportsobjectfunctionloadWebAssembly(filename,imports){//Fetchthefileandcompileitreturnfetch(filename).then(response=>response.arrayBuffer()).then(buffer=>WebAssemblyer.compile)then(module=>{//Createtheimportsforthemodule,包括//标准动态库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=newWebAssembly.Memory({initial:256});}if(!imports.env.table){imports.env.table=newWebAssembly。Table({initial:0,element:'anyfunc'});}//Createtheinstance.returnnewWebAssembly.Instance(module,imports);});}我们可以使用上面的工具函数加载wasm文件:loadWebAssembly('counter.wasm').then(实例=>{varexports=instance.exports;//theexportsthatinstancevarcount=exports._count;//"_count"函数(注意"_"前缀)//下面可以调用count函数});而在作者的脚手架中,使用wasm-loader进行Load,这样就可以直接将wasm打包到Bundle中,然后通过import导入:importReact,{PureComponent}from"react";importCounterWASMfrom./counter.wasm";importButtonfrom"antd/es/button/button";import"./Counter.scss";/***描述简单计数器示例*/exportdefaultclassCounterextendsPureComponent{state={count:0};componentDidMount(){this.counter=newCounterWASM({env:{memoryBase:0,tableBase:0,memory:newwindow.WebAssembly.Memory({initial:256}),table:newwindow.WebAssembly.Table({initial:0,element:"anyfunc"})}});this.setState({count:this.counter.exports._count()});}/***描述默认渲染函数*/render(){constisWASMSupport="WebAssembly"inwindow;if(!isWASMSupport){return(

浏览器不支持WASM
);}return(简单计数器反例:{this.state.count}{this.setState({count:this.counter.exports._count()});}}>点击自增
);}}使用wasm-loader时,会调用newWebAssembly.Instance(module,importObject);module是WebAssembly.ModuleimportObject的实例,即wasm-loader提供的默认对象。简单的游戏引擎重构上面我们讨论了使用WebAssembly重构一个简单的计数器模块。这里我们以一个简单的游戏为例,来交互感受WebAssembly带来的性能提升。您可以直接查看游戏的在线演示。这里的游戏引擎就是进行部分计算和重新赋值的操作。比如这里计算下一个位置状态的函数在C中的实现为:EMSCRIPTEN_KEEPALIVEvoidcomputeNextState(){loopCurrentState();intneighbors=0;inti_m1,i_p1,i_;intj_m1,j_p1;intheight_limit=height-1;intwidth_limit=width-1;for(inti=1;i{letbuffer=newArrayBuffer(size);returnnewUint8Array(buffer);},_memcpy:(source,target,size)=>{letsourceEnd=source.byteLength;leti,j;for((i=0),(j=0),(k=newUint8Array(target)),(l=newUint8Array(source));i