webassembly magic photical performs pixie dust?

2021-04-14 14:23:15

令人难以置信的不满意的答案是:它取决于。这取决于哦 - 如此多的因素,我将在这里抚摸其中一些。

我真的很喜欢大会(完整披露:我是他们的支持者之一)。这是一种非常年轻的语言,具有一个小但激情的团队,为目标网址定位的类型语言构建了自定义编译器。我喜欢的原因是assemblyscript(或shart for shart)是因为它允许平均web开发人员使用webassembly,而无需学习潜在的新语言,如C ++或Rust。值得注意的是,语言是拼写的。不要指望您现有的打字代码只能在框中编译。已经说,语言是有意镜像类型签字的行为和语义(以及因此JavaScript),这意味着“将”打字标注字样的动作通常主要是添加类型的注释。

我总是想知道是否有什么可以获得一块javascript的东西,将它转化为组装并将其编译为webassembly。当我的同事ingvar向我发了一块javascript代码来模糊图像,我认为这将是一个完美的案例研究。我跑了一个快速的实验,看看它是否值得深入探索将JavaScript移植到组装。哦,男孩是值得的。本文是更深入的探索。

如果您想了解更多关于AssemblyScript的信息,请访问网站,加入Discord,或者如果您喜欢,请观看我用播客丈夫Jake制作的大会介绍视频。

我认为这是公平的,可以说是Webassembly的最成熟用例正在进入其他语言的生态系统。例如,在SquoSh中,我们使用C / C ++和Rust Ecosystem的库来处理图像。这些库没有用网页写入,而是通过Webassembly,他们可以在那里运行。

在我的看法中,WebAssembly也与很多人的表现强烈相关。它旨在快速,它编译,所以它必须快速,对吧?好吧,在最长的时间,我一直被网址和javascript具有相同的峰值性能,而且我仍然落后于此。考虑到理想条件,它们都编译为机器代码,并最终同样快速。但是,这里显然有更多细微差别,当有条件在网页上是理想的,我认为如果我们认为WebAsseMbly是一种获得更可靠的性能的方法会更好。

但是,实现WebasseMbly最近一直访问JavaScript无法使用的性能原语(如SIMD或共享内存线程)也很重要,这使得Webassembly增加了外出JavaScript的机会。还有一些其他Qualassembly的品质可能使其更适合特定情况,而不是JavaScript:

对于v8来执行javascript,它首先将代码提供给解释器“点火”。点火进行了优化,可以尽快运行代码。之后,“SparkPlug”采用点火输出(臭名昭着的“字节码”)并将其转换为非优化的机器代码,以提高内存占用的成本产生更好的性能。虽然代码正在执行,但v8密切关注它以收集对象形状的数据(请将其视为类型)。一旦收集了足够的数据,V8的优化编译器“TurboOman”启动并生成了为这些类型进行优化的低级机器代码。这将提供另一种显着的速度增强。

这一切都在下降:如果你想了解更多关于精确权衡的信息,我可以通过Benedikt和Mathias推荐这篇文章。

另一方面,WebAsseMbly是强烈打字的。它可以直接转向机器代码。 V8有一个名为“升降机”的流媒体编译器,它像点火一样,用于使您的代码快速运行,以产生潜在的次优执行速度。第二个升降机进行了完成,TurboOman踢出并生成优化的机器代码,它将比产生的升降机更快地运行,但会更长时间才能生成。与JavaScript的大区别是Turboofan可以在不必先观察您的母鹿的情况下进行工作。

TurboOman为JavaScript生成的机器代码仅适用于Types Hold的假设。如果TurboOman为函数f生成的机器代码f具有一个数字作为参数,并且现在突然函数f称为对象调用,发动机必须倒回点火或火花。这被称为“去优化”(或“缩短”)。同样,因为WebAsseMbly强烈键入,所以类型无法改变。不仅如此,而是旨在将Webassembly支持的类型旨在映射到机器代码。 Webassembly无法发生。

现在这个有点难以捉摸。根据WebAsseMbly.org,“WASM堆栈机器设计用于以尺寸和负载节省的二进制格式编码。”然而,webassembly目前对于生成大二元斑点时,至少是在网上被认为是“大”的臭名昭着的。 Webassembly压缩非常好(通过gzip或brotli),可以撤消很多膨胀。

很容易忘记JavaScript附带大量电池(尽管它没有标准库)。例如:您可以处理数组,对象,迭代键和值,拆分字符串,过滤器,映射,具有原型继承等等等。所有内置于JavaScript引擎中的所有内容。除算术之外,WebAssembly无所作为。每当您在编译到WebasseMbly的语言中使用这些更高级别的概念中的任何一个,都必须将其捆绑到您的二进制文件中,这是大网上装配二进制文件的原因之一。当然,那些函数只需要包括一次,因此更大的项目将从WASM的小型二进制表达中受益,而不是小模块。

并非所有这些优点在任何特定的情况下都同样可用或重要。但是,已知大会旨在生成相当小的Webassembly二进制文件,我很好奇如何在速度和大小方面具有直接可比较的JavaScript。

如上所述,组装MIMICS MIMICS的语义和Web平台API尽可能多,这意味着移植一块JS以ASC主要是将类型注释添加到代码的问题。作为第一个例子,我拍了glur,是一个迷惑图像的JavaScript库。

ASC的内置类型镜像WebasseMbly VM的类型。虽然键盘标记中的数值只是数字(根据规范的64位IEEE754浮动),assemblyscript具有U8,U16,U32,I8,I16,I32,F32和F64作为其原始类型。 ASC的小小无于强大的标准库添加了串,阵列<,arratsbuffer,uint8Array等的更高级别的数据结构。唯一的ASC特定数据结构,即既不是JavaScript也不是Web平台史蒂塔雷,我会稍后谈谈。

作为一个例子,这里是来自Glur库及其组装的功能:

功能Gausscoef(Sigma){if(sigma< 0.5)sigma = 0.5; var a =数学。 EXP(0.726 * 0.726)/ Sigma; / * ...更多Math ... * /返回新([A0,A1,A2,A3,B1,B2,Left_Corner,Right_Corner]); }

功能Gausscoef(Sigma:F32):Float32Array {if(sigma <0.5)sigma = 0.5;让答:f32 = mathf。 EXP(0.726 * 0.726)/ SIGMA; / * ...更多数学... * / const r = new(8); const v = [a0,a1,a2,a3,b1,b2,left_corner,right_corner]; for(让我= 0; i&lt; length; i ++){r [i] = v [i]; }返回r; }

填充数组的末尾的显式循环是因为当前在汇编中的电流短圈:函数重载不支持。 ASC仅存在float32Array的一个构造函数,这为TypedArray的长度带来了I32参数。按ASC支持回调,但闭包也不是,所以我不能使用.Foreach()填写值。这肯定是不方便的,但并不是如此。

Mathf:你可能已经注意到了Mathf而不是数学。 Mathf专门用于32位浮点数,而数学是64位浮点数。我可以使用数学并完成演员,但由于所需的精度增加,它们略微略微稍慢。无论哪种方式,高斯库函数都不是热路径的一部分,因此它真的没有差异。

让我难以弄清楚的东西是弄清楚的是,呃,类型。模糊图像涉及卷积,这意味着整个围绕所有像素迭代。我认为,因为所有像素指数都是正的,因此循环计数器也是如此,并决定为那些环路变量选择U32。如果循环发生任何循环,那将用一个可爱的无限循环咬你,如下次数:

让J:U32; // ......许多代码行...对于(j =宽度 - 1; j&gt; = 0; j - ){// ...}

除此之外,移植JS到ASC的行为是一个漂亮的机械任务。

既然我们有一个JS文件和ASC文件,我们可以将ASC编译为webassembly并运行一点基准测试来比较运行时性能。

d - 何种内容?:D8是v8周围的最小CLI包装器,暴露了对WASM和JS的各种发动机功能的细粒度控制。您可以像节点一样想到它,但没有任何标准库。只是香草ECMAScript。除非您在本地编译v8(通过沿V8.dev上的指南执行该指南),否则您可能不会有D8可用。 JSVU是一种工具,可以为许多JavaScript引擎安装预编译的二进制文件,包括V8。

然而,由于这个部分在标题中有“基准标记”这个词,我认为在这里放弃一个免责声明是很重要的:我在这里列出的数字是我用我选择的语言写入的代码,ran在我的机器上运行(一个2020 M1 MacBook空气)使用我所做的基准脚本。结果最多是粗略指标,并不是毫无意义地从中获取关于组装,WebAssembly或JavaScript的一般性表现的定量结论。

有些人可能想知道为什么我正在使用D8而不是在浏览器中甚至节点运行它。两个节点和浏览器都有,...其他可能或可能无法拧任何结果的其他东西。 D8是我可以获得最无菌的环境,作为顶部的樱桃,它允许我控制分层行为。我可以限制执行以使用点火,Sparkplug或升降机,确保性能特征不会在基准中间变化。

如上所述,在基准测试时,“预热”JavaScript非常重要,使V8有机会优化它。如果您不这样做,您可能会最终最终测量解释JS和优化机器代码的性能特征的混合。为此,我在开始测量之前5次运行模糊程序,然后我做了50个定时运行并忽略5最快,最慢的运行以删除潜在的异常值。这是我得到的:

一方面,我很高兴看到升降机的输出比点火或火花可以从JavaScript中挤出的速度更快。与此同时,优化的webassembly模块只要JavaScript长3倍,它并没有很好。

要公平,这是一个David VS Goliath情景:V8是一个长期以来的JavaScript引擎,具有一支巨大的工程师,实现优化和其他聪明的东西,而组装是一个相对年轻的项目,围绕它的小团队。 ASC的编译器是单通过,持续到Binaryen的所有优化工作(另请参阅:WASM-opt)。这意味着在大多数高级语义被编译后,优化在WASM VM字节码级别完成。 V8在这里有一个清晰的边缘。但是,模糊代码如此简单 - 只是从内存中执行算术 - 我真的期待它更近。这里发生了什么?

快速咨询V8团队的一些人和大会团队的一些人(谢谢Daniel和Max!),事实证明这里的一个大区别是“界检查” - 或缺乏。

V8具有访问原始JavaScript代码和关于语言语义的知识的奢侈品。它可以使用该信息来应用其他优化。例如:它可以告诉您不仅仅是从内存中读取值的不随机读取值,但是您正在使用循环的循环的ArrayBuffer迭代。有什么不同?对于循环的一个...,语言语义保证您永远不会尝试读取阵列缓冲外的值。当缓冲区仅为10个字节时,您永远不会意外读取字节11,或者:您永远不会超出界限。这意味着TurboOman不需要发出界限检查,您可以考虑到确保您不访问内存的语句您不应该。这类信息丢失一旦编译为网页,而且由于ASC的优化只发生在WebasseMbly VM级别,因此不一定应用相同的优化。

幸运的是,AssemblyScript提供了一种魔法未选中()注释,以表明我们负责留下界限。

- prev_prev_out_r = prev_src_r * coeff [6]; - 行[line_index] = prev_out_r; + prev_prev_out_r = prev_src_r *未选中(coeff [6]); +未选中(行[line_index] = prev_out_r);

但是还有更多:assemblyscript的键入数组(UInt8Array,Float32Array,...)在平台上完成相同的API,这意味着它们仅在底层ArrayBuffer上的视图。这是良好的,因为API设计熟悉并进行了战斗,但由于缺乏高级别的优化,这意味着每次访问键入的阵列上的字段(如MyFloatarray [23])需要两次访问内存:一旦将指针加载到此特定阵列的基础阵列缓冲区,另一个要在正确的偏移量上加载值。 V8,因为它可以告诉您访问键入的阵列但从来没有底层缓冲区,很可能能够优化整个数据结构,以便您可以使用单个内存访问读取值。

因此,AssemblyScript提供了StatyArray&lt; t&gt;,它大多等于阵列&lt; t&gt;除了它不能成长。通过固定长度,不需要将数组实体与存储器分开,这些值存储在内存中,从而消除该间接。

好了很多!虽然组装仍然比JavaScript慢,但我们显着更近。这是我们能做的最好的吗?

对我指出的组件的另一件事是--Optimize标志等同于-O3积极优化速度,而是使权衡降低二元尺寸。 -O3仅优化速度和速度。在-O3s作为默认的灵感良好 - 网上的二元尺寸很重要 - 但它是值得的吗?至少在这个具体的例子中,答案是否定的:-O3最终交易巨大的绩效惩罚的可笑量〜30字节:

一个单一优化器标志呈夜间差异,让装配脚本超越JavaScript(在此特定的测试用例!)。我们制作的大会比JavaScript更快!

为了获得一些信心,图像模糊例子不仅仅是一种侥幸,我以为我应该用第二个程序再次尝试这一点。相反,我采取了冒泡性地从stackoverflow的冒泡来实现,并通过相同的进程运行:添加类型。运行基准。优化。运行基准。阵列的创建和群体是泡沫排序的不是基准代码路径的一部分。

我们再次这样做了!这次具有甚至更大的差异:优化的综合脚本几乎是JavaScript的两倍。但我帮忙:现在不要停止阅读。

有些人可能已经注意到这两个例子都非常少或没有分配。 V8为您提供JavaScript中的所有内存管理(和垃圾收集),我不会假装我对此了解。另一方面,在WebAssAssembly中,您可以获得一大块线性内存,您必须决定如何使用它(或者更确切地说:语言)。如果我们大量使用动态内存,这些排名会如何发生多少?

为了测量这一点,我选择基准测试二进制堆的实现。基准测试填充了100万随机数(Math.Random()的礼貌)填充二进制堆)并将其全部退回,检查数字是否按顺序增加。该过程仍然与上面相同:使JS代码的Naïve端口进行ASC,运行基准,优化,再次进行基准:

80x比JavaScript慢?!甚至比点火慢?当然,这里还有其他东西出现问题。

我们在组件中创建的所有数据都需要存储在内存中。要确保我们不会覆盖已在内存中的其他任何内容,都有内存管理。随着AssemblyScript旨在提供熟悉的环境,镜像JavaScript的行为,它会将一个完全托管的垃圾收集器添加到WebasseMbly模块,以便您不必担心何时分配以及何时释放内存。

默认情况下,汇票船用双级分离拟合存储器分配器和增量三色标记&amp;扫描(ITCMS)垃圾收集器。它实际上与本文实际上并不重要,他们使用的是哪种分配器和垃圾收集器,我刚才发现您可以看看它们的有趣。

这个默认的运行时,称为增量,令人惊讶的是,只增加大约2KB的GZIP的网页主义。 AssemblyScript还提供替代的运行时间,即可以使用--Runtime标志选择的最小和存根。 MIMIMAL使用相同的分配器,而是一个更轻量级的GC,不自动运行,但必须手动调用。对于在GC将暂停程序时,您希望在您想要控制的游戏中,这可能是有趣的。存根非常小(〜400b gzip),快速,因为它只是一个凹凸分配器。

我可爱的记忆凹凸:凹凸分配器非常快,但缺乏释放记忆的能力。虽然这听起来很愚蠢,但对于单一的模块来说,这可能非常有用,而不是释放内存,则删除整个WebasseMbly实例并相当创建一个新的。如果您很好奇,我实际上在我的文章中编写了一个凹凸分配器,在没有eMscripten的情况下编译到webassembly。

最小和存根都会让我们显着更接近JavaScripts性能。但为什么这两个太快了?如上所述,最小和增量的共享相同的分配器,因此不能成为它。两者也有一个垃圾收集器,但除非明确地调用(我们并不调用它,否则最小的运行它这意味着差异化的质量是增量运行垃圾收集,而最小值和短卷则不会。我不明白为什么垃圾收集器应该使这种差异很大,考虑到它必须跟踪一个阵列。

在使用调试符号(--debug)的构建中使用D8进行了一些分析,事实证明,在系统库Libsystem_Platform.Dylib中花费了大量时间,其中包含用于线程和内存管理的OS级原语。进入这个库的呼叫是由__new和__gene制作的,从阵列又称r; f32&gt; #push:

[自下而上(重)配置文件]:滴答父项名称18670 96.1%/usr/lib/system/libsystem_platform.dylib 13530 72.5%函数:*〜lib / RT / ITCMS / __续签13530 100.0%Func

......