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

WebAssembly的现状

时间:2023-03-12 09:31:08 科技观察

上篇文章《关于WebAssembly 的背景知识》让你对WebAssembly有了基本的了解。接下来,我们将继续介绍WebAssembly的工作原理以及WebAssembly运行速度更快的原因。一、WebAssembly的工作原理WebAssembly是除JavaScript之外的另一种可以在网页中运行的编程语言。在过去,如果你想在浏览器中运行代码来控制网页中的各种元素,JavaScript是唯一的选择。所以当人们谈论WebAssembly时,他们倾向于将其与JavaScript进行比较。但它们并不是真正的“非此即彼”关系——不仅仅是WebAssembly或JavaScript。事实上,我们鼓励开发者一起使用这两种语言。即使你不自己实现WebAssembly模块,你也可以学习它现有的模块,利用它的优势来实现你的功能。WebAssembly模块定义的一些函数可以从JavaScript调用。所以就像你可以通过npm下载lodash模块并通过API使用它一样,未来你也可以下载WebAssembly模块并使用它提供的功能。那么,让我们来看看如何开发WebAssembly模块并通过JavaScript使用它们。1.WebAssembly在哪里?在上一篇WebAssembly背景知识的文章中,介绍了编译器是如何将高级语言翻译成机器码的。那么上图中的WebAssembly在哪里呢?实际上,您可以将其视为另一种“目标汇编语言”。每种目标汇编语言(x86、ARM)都依赖于特定的机器架构。当你想在用户机器上执行你的代码时,你不知道目标机器的结构是什么。与其他汇编语言不同,WebAssembly不依赖于特定的物理机器。可以抽象地理解为概念机器的机器语言,而不是实际物理机器的机器语言。正因为如此,WebAssembly指令有时被称为虚拟指令。它比JavaScript代码更直接地映射到机器代码,它也代表了一种如何在通用硬件上更高效地执行代码的想法。所以它不会直接映射到特定于硬件的机器代码。浏览器下载WebAssembly后,可以快速将其转换为机器汇编代码。2.编译成.wasm文件目前对WebAssembly支持最好的编译器工具链是LLVM。有许多不同的前端和后端插件可用于LLVM。提示:许多WebAssembly开发人员使用C或Rust进行开发,然后编译为WebAssembly。实际上还有其他方法可以开发WebAssembly模块。比如使用TypeScript开发WebAssembly模块,或者直接使用文本格式的WebAssembly。假设我们要从C语言转到WebAssembly,我们需要clang前端将C代码转换为LLVM中间代码。当转换为LLVMIR时,说明LLVM已经理解了代码,它会自动优化代码。为了从LLVMIR生成WebAssembly,还需要后端编译器。LLVM项目中有后端正在开发中,应该很快就会开发出来。目前,还无法看到它是如何工作的。还有一个易于使用的工具,称为Emscripten。它通过自己的后端将代码转化为自己的中间代码(称为asm.js),然后转化为WebAssembly。事实上它背后也使用了LLVM。Emscripten还包含许多其他工具和库以包含整个C/C++代码库,因此它更像是一个软件开发工具包(SDK)而不是编译器。比如系统开发者需要一个文件系统来读写文件,而Emscripten有一个IndexedDB来模拟文件系统。不用过多考虑这些工具链,只要知道最终会生成一个.wasm文件即可。后面会介绍.wasm文件的结构。在此之前,我们先看看如何在JS中使用它。3.将.wasm模块加载到JavaScript中。wasm文件是一个WebAssembly模块,可以加载到JavaScript中使用。这个阶段的加载过程有点复杂。functionfetchAndInstantiate(url,importObject){returnfetch(url).then(response=>response.arrayBuffer()).then(bytes=>WebAssembly.instantiate(bytes,importObject)).then(results=>results.instance);}如果您想深入挖掘,可以在MDN文档中了解更多信息。我们一直致力于简化此过程并优化工具链。希望可以集成到现有的模块打包工具中,比如webpack,或者集成到loader中,比如SystemJS。我们相信加载WebAssembly模块也可以像加载JavaScript一样简单。以下是WebAssembly模块和JavaScript模块之间的主要区别。目前WebAssembly只能使用数字(整数或浮点数)作为参数或返回值。对于任何其他复杂类型,例如字符串,您必须使用WebAssembly模块的内存操作。如果你经常使用JavaScript,对直接内存操作不是很熟悉,可以回忆一下C、C++、Rust等语言,它们都是手动操作内存的。WebAssembly的内存操作与这些语言非常相似。为此,它使用JavaScript中称为ArrayBuffer的数据结构。ArrayBuffer是一个字节数组,所以它的索引(index)就相当于内存地址。如果你想在JavaScript和WebAssembly之间传递一个字符串,你可以使用ArrayBuffer将它写入内存。此时ArrayBuffer的索引是一个整数,你可以将它传递给WebAssembly函数。此时,第一个字符的索引可以作为一个指针。这就像当一个网络开发者开发一个WebAssembly模块时,他给这个模块包裹了一件外套。这样其他用户在使用这个模块时就不需要关心内存管理的细节了。如果您想了解有关内存管理的更多信息,请查看我们编写的WebAssembly中的内存操作。4..wasmfilestructureIfyouareadeveloperwhowritesahigh-levellanguageandcompilesitintoWebAssemblythroughacompiler,thenyoudon'tneedtocareaboutthestructureoftheWebAssemblymodule.Butunderstandingitsstructurehelpsyouunderstandsomefundamentalissues.Ifyoudon'tknowthecompileryet,Isuggestyoureadthearticle"Series3:HowCompilersGenerateAssembly".ThiscodeistheCcodethatwillgenerateWebAssembly:intadd42(intnum){returnnum+42;}YoucanuseWASMExplorer来编译这个函数。打开.wasm文件(假设你的编辑器支持的话),可以看到下面代码:0061736D0D0000000186808080000160017F017F0382808080000100048480808000017000000583808080000100010681808080000007968080800002066D656D6F72790200095F***3561646434326900000A8D80808000018780808000002000412A6A0B这是模块的“二进制”表示。之所以用引号把“二进制”引起Come,becausetheaboveisactuallyrepresentedinhexadecimal,butitisalsoeasytoconvertitintobinaryordecimalrepresentationthatpeoplecanunderstand.Forexample,thefollowingarevariousrepresentationsofnum+42.5.ThecodeisHowitworks:Stack-basedvirtualmachineIfyouarecuriousaboutthespecificoperationprocess,thenthispicturecantellyouwhattheinstructionsdo.Fromthefigurewecannoticethattheadditionoperationdoesnotspecifywhichtwonumberstoadd.ThisisbecauseWebAssemblyusesa"stack-basedvirtualmachine"mechanism.Thatis,allthevalues????neededbyanoperatorarestoredonthestackbeforetheoperationisperformed.Alloperators,suchasaddition,knowthattheyneedHowmanyvalues.Addneedstwovalues,soitjusttakestwovalues??fromthetopofthestack.Thentheaddinstructioncanbeshortened(singlebyte),becausetheinstructiondoesnotneedtospecifythesourceregisterandthedestinationregister.ThisItalsomakesthe.wasmfilesmaller,whichinturnmakesloadingthe.wasmfilefaster.AlthoughWebAssemblyusesastack-basedvirtualmachine,itdoesn'tmeanthatitworkslikethisontheactualphysicalmachine.当浏览器将WebAssembly翻译成机器码时,浏览器会使用寄存器,而WebAssembly代码并没有指定使用哪些寄存器。这样做的好处是给了浏览器最大的自由度,让它自己进行注册激活。*分发。6.WebAssembly模块的组件除了上述之外,.wasm文件还有其他部分。有些组件是模块必需的,有些是可选的。必填部分:类型。模块中定义的函数的函数声明和所有导入函数的函数声明。功能。给模块中的每个函数一个索引。代码。模块中每个函数的实际函数体。可选部分:导出。使函数、内存、表、全局变量等对其他WebAssembly或JavaScript可见,允许动态链接一些单独编译的组件,WebAssembly版本的.dll。进口。允许从其他WebAssembly或JavaScript导入指定的函数、内存、表或全局变量。开始。一个可以在加载WebAssembly模块时自动运行的函数(类似于main函数)。全球的。声明模块的全局变量。记忆。定义模块使用的内存。桌子。启用映射到WebAssembly模块外部的值,例如JavaScript对象。这对于间接函数调用很有用。数据。初始化导入或本地内存。元素。初始化导入或本地表。如果您想了解有关这些组件的更多信息,可以阅读这些组件的工作原理。2.为什么WebAssembly更快?上面我介绍了如何编写WebAssembly程序,也表达了我希望看到更多的开发者在他们的项目中同时使用WebAssembly和JavaScript。开发者不必纠结是选择WebAssembly还是JavaScript。已经有JavaScript项目的开发者希望用WebAssembly替换部分JavaScript来尝试。例如,开发React应用程序的团队可以用WebAssembly版本替换协调代码(即虚拟DOM)。而对于你的web应用程序的用户来说,他们像以前一样使用它,没有任何变化,同时他们可以享受到WebAssembly带来的好处——快速。开发者之所以选择替代WebAssembly,是因为WebAssembly速度更快。1.目前的JavaScript性能如何?在了解JavaScript和WebAssembly之间的性能差异之前,我们需要了解JS引擎的工作原理。下图显示了性能使用的大致分布。JS引擎在图形的各个部分花费的时间取决于页面使用的JavaScript代码。图表中的比例并不代表实际情况中的确切比例。图中每个颜色条代表不同的任务:Parsing——代表将源代码变成解释器可以运行的代码所花费的时间;编译+优化——代表基线编译器和优化编译器所花费的时间。一些优化编译器作业不在主线程上运行,因此不在此处介绍。重新优化-当JIT发现优化假设错误时,花费在丢弃优化代码上的时间。包括重新优化、丢弃和返回基线编译器的时间。Execution——TimetoexecutecodeGarbagecollection——垃圾收集,清理内存的时间这里注意:这些任务不是离散执行的,也不是按照固定的顺序依次执行的。相反,它是交叉执行,这样在解析过程进行时,一些其他代码正在运行,一些其他代码正在编译。这样的交叉执行大大提高了早期JavaScript的效率。早期的JavaScript执行类似于下图,各个过程是顺序进行的:早期的JavaScript只有解释器,执行起来非常慢。引入JIT后,大大提高了执行效率,缩短了执行时间。JIT支付的开销是监控代码和编译时间。JavaScript开发者可以像以前一样开发JavaScript程序,解析和编译同一个程序的时间也大大缩短。这使得开发人员更倾向于开发更复杂的JavaScript应用程序。同时,这也说明执行效率还有很大的提升空间。2.WebAssembly的对比下面是WebAssembly与典型Web应用的大致对比:上图中各种浏览器处理的流程不同,存在细微差别。以SpiderMonkey为例。3、获取文件的步骤图中没有显示,但是从服务器获取文件看似简单的一步,但是要花很长时间。WebAssembly比JavaScript具有更高的压缩比,因此文件获取也更快。尽管压缩算法可以显着减少JavaScript的包大小,但压缩后的WebAssembly二进制代码仍然更小。这意味着在服务器和客户端之间传输文件的速度更快,尤其是在网络状况不佳的情况下。4.解析当到达浏览器时,JavaScript源代码被解析成一个抽象语法树。浏览器采用懒加载的方式,只解析真正需要的部分,对于浏览器暂时不需要的功能只保留其存根)。AST(抽象语法树)解析后成为中间代码(称为字节码),提供给JS引擎编译。另一方面,WebAssembly不需要这种转换,因为它本身就是中间代码。它所要做的就是解码并检查代码是否没有错误。5.编译和优化在JIT一文中,我介绍了JavaScript是在代码的执行阶段进行编译的。因为是弱类型语言,当变量类型发生变化时,同样的代码会被编译成不同的版本。不同的浏览器以不同的方式处理WebAssembly的编译过程。一些浏览器只对WebAssembly进行基线编译,而另一些浏览器则使用JIT进行编译。无论哪种方式,WebAssembly都更接近机器代码,因此速度更快,其速度更快的原因有几个:在编译优化代码之前,它不需要提前运行代码来了解变量的类型。编译器不需要编译相同代码的不同版本。很多优化在LLVM阶段就已经完成了,所以在编译优化的时候没有太多的优化可做。6.重新优化在某些情况下,JIT会反复执行“放弃优化代码<->重新优化”的过程。当JIT在优化假设阶段所做的假设在执行阶段被发现不正确时,就会发生这种情况。例如,当循环发现本次循环使用的变量类型与上一次循环不同,或者在原型链中插入了新的函数,都会导致JIT丢弃优化后的代码。去优化过程有两部分开销。***,放弃优化代码并回到基线版本需要时间。其次,如果函数仍然被频繁调用,JIT可能会再次将其发送给优化编译器,并再次进行优化编译,这是在做无用功。在WebAssembly中,类型是确定的,所以JIT不需要根据变量的类型做优化假设。也就是说,WebAssembly没有再优化阶段。7、执行本身也可以编写执行效率高的JavaScript代码。你需要了解JIT的优化机制,比如你需要知道什么样的代码编译器会对其进行特殊处理(在JIT一文中有提到)。然而,大多数开发人员并不知道JIT的内部实现机制。即使开发者知道了JIT的内部机制,也很难写出符合JIT标准的代码,因为人们通常为了更好的代码可读性而采用的编码方式恰恰不适合编译器对代码进行优化。另外,JIT会针对不同的浏览器做不同的优化,所以针对一种浏览器优化的比较好,而在另一种浏览器上很可能效率就比较低。因此,WebAssembly的执行速度通常更快,并且JIT为JavaScript所做的许多优化在WebAssembly中并不需要。另外,WebAssembly是为编译器设计的,开发者不直接对其进行编程,这样WebAssembly可以专注于为机器提供更理想的指令(执行效率更高的指令)。在执行效率方面,不同的代码函数有不同的效果。一般来说,执行效率会提高10%~800%。8.垃圾回收在JavaScript中,开发者不需要手动清理内存中不用的变量。JS引擎会自动做这个,这个过程叫做垃圾回收。然而,当你想达到可控的性能时,垃圾回收可能会成为一个问题。垃圾收集器自动启动,不受您的控制,因此很有可能在不合时宜的时间启动。目前的大部分浏览器已经可以为垃圾回收安排一个合理的启动时间,但这仍然会增加代码执行的开销。截至目前,WebAssembly不支持垃圾回收。内存操作都是手动控制的(像C、C++)。这确实增加了开发者的一些开发成本,但也让代码执行效率更高。9.得出WebAssembly比JavaScript执行得更快的结论是因为:在文件抓取阶段,WebAssembly抓取文件的速度比JavaScript快。即使JavaScript被压缩,WebAssembly文件也比JavaScript小;在解析阶段,WebAssembly的解码时间比JavaScript短;在编译优化阶段,WebAssembly有优势,因为WebAssembly的代码更接近机器码,而JavaScript则需要先在服务器端进行优化。在重新优化阶段,WebAssembly不会被重新优化。但是JS引擎的优化假设可能会造成“放弃优化代码<->重新优化”的现象。在执行阶段,WebAssembly速度更快,因为开发人员不需要了解JavaScript所需的那么多编译器技巧。WebAssembly代码也更适合生成机器执行效率更高的指令。在垃圾回收阶段,WebAssembly的垃圾回收是人工控制的,比自动回收效率更高。这就是为什么在大多数情况下,对于相同的任务,WebAssembly比JavaScript表现更好。然而,仍然有一些情况下WebAssembly不会按预期执行;而WebAssembly的未来也会朝着让WebAssembly执行更高效的方向发展。这些我会在下一篇文章介绍《WebAssembly 的现在与未来》。点击《WebAssembly 系列(四)WebAssembly 工作原理》和《WebAssembly 系列(五)为什么 WebAssembly 更快?》阅读原文。【本文为专栏作者“虎子打哈”原创文章,转载请联系作者获得授权】点此阅读更多该作者好文