也许你不需要Rust和WASM来加速你的JS(2018)

2021-06-24 23:17:18

几周前我注意到一个博客文章“氧化源地图”在Twitter上进行回合 - 谈论替换JavaScript的核心源代码效益,并使用Rust VersionCombiled To Webassembly。

这篇文章激起了我的兴趣,不是因为我是一个巨大的生锈或母舰,而是因为我总是很奇怪的语言特征和优化在纯粹的JavaScript中缺少,以实现类似的性能。

所以我从GitHub检查了图书馆,并在小型的PerformentSporeigation上离开了,我几乎逐字在这里记录。

对于我的调查,我使用的是几乎默认的x64.Release在Commit 69ABB960C97606DF99408E6869D660014AA0FB51中的备用X64。我唯一的离开默认配置是通过GN标志的IENable Disassembler,如果需要,可以潜入到生成的Machinecode。

╭─〜/ src / V8 / v8 ╰─ =真的

╭─〜/ src / source-map / bechch ╰─$ d8 bench-shell-bindings.js解析源mapconsole.iment:reateration,4655.638000console.timend:迭代,4751.12200console.imeend:迭代,4820.566000console。 Timeend:迭代,4996.942000Console.TimeInd:迭代,4644.619000 [统计样品:5,总计:23868 MS,平均:4773.6 ms,STDDEV:161.22112144505135 MS]

diff --git a / bench / bench-shell绑定.js b / bench / bench-shell-bindings.jsindex 811df40..c97d38b 100644 --- a / bench / bench-shell-bindings.js +++ b / bench / bench -shell-bindings.js @@ -19,5 +19,5 @@ load(" ./ bench.js");打印("解析源地图");打印(BenchmarkParsesourcemap()); print(); - 打印("序列化源地图"); - print(benchmarkserializeourcemap()); + //打印("序列化源地图"); + //打印(BenchmarkSerializesOrCemap ());

╭─〜/ src / source-map / bench ╰─$ perf record-g d8 -perf-basic-prof bench-shell-bindings.js解析源mapconsole.iment:迭代,4984.464000 ^ c [ Perf Record:唤醒90次写入数据] [Perf Record:捕获并写作24.659 MB Perf.data(〜1077375样本)]

请注意,我将-perf-basic-prof标志传递给d8二进制文件v8以生成辅助映射文件/tmp/perf-whpid.map。此文件允许Perf报告了解JIT生成的机器代码。

这是我们从PET报告中获得的东西 - 在放大主执行线程后:

架空符号17.02%* doquicksort ../dist/source-map.js:2752 11.20%buildin:arguptsadaptortrampoline 7.17%* compareByoriginalPositions ../dist/source-map.js:1024 4.49%buildin:callfunction_receiverisnullorundefined 3.58%* compareygeneratedsdeflated. /dist/source-map.js:1063 2.73%* sourcemapconsumer_parsemappings ../dist/source-map.js:1894 2.11%buildin:stringequal 1.93%* sourcemapconsumer_parsemappings ../dist/source-map.js:1894 1.66%* doquicksort ../dist/source-map.js:2752 1.25%v8 ::内部:: scringtable :: lookupstringifexists_noallocate 1.22%* sourcemapconsumer_parsemappings ../dist/source-map.js:1894 1.21%buildin:StringCharat 1.16%内置: call_receiverisnullorundefined 1.14%v8 ::内部:(匿名命名空间):: stringTableNoAllocateKey :: Ismatch 0.90%内置:StringPrototyPeslice 0.86%内置:KeyedloadIC_Megamorphic 0.82%V8 ::内部::(匿名命名空间):: MakEstringthin 0.80%V8 ::内部::(匿名命名空间):: CopyObjectToObjectElements 0.76%V8 :: Internal :: Scaven ger :: scavengobject 0.72%v8 ::内部:: string :: scateflat< v8 ::内部:: iterationStringhasher> 0.68%* sourcemapconsumer_parsemappings ../dist/source-map.js:1894 0.64%* doquicksort ../dist/source-map.js:2752 0.56%v8 ::内部:: increntmentalmarking :: recordwriteLow

实际上,就像“氧化来源地图......”帖子已经说过那个相当沉重的帖子,如此:Doquicksort出现在个人资料中的顶部AndAlso(这意味着它被优化/取出筹码时间)。

在探查器中跳出的一件事是可疑条目,即“内在的条目:ArguptsAdvertrampoline和内置:CallFunction_ReceiverisNullorundefinedwhile似乎是V8实现的一部分。如果我们要求将PERF报告扩展到扩展到它们,那么我们会注意到这些函数是从分类代码中调用的Alsomostly:

- 构成:ArgumentsAdaptorRampoline + 96.87%* doquicksort ../dist/source-map.js:2752 + 1.22%* sourcemapconsumer_parsemappings ../dist/source-map.js:1894 + 0.68%* sourcemapconsumer_parsemappings ../dist/source- map.js:1894 + 0.68%内置:解释仪Enterrytrampoline + 0.55%* doquicksort ../dist/source-map.js:2752-内置:callfunction_receiverisnullorundefined + 93.88%* doquicksort ../dist/source-map.js:2752 + 2.24%* sourcemapconsumer_parsemappings ../dist/source-map.js:1894 + 2.01%内置:解释仪Enterrytrampoline + 1.49%* sourcemapconsumer_parsemappings ../dist/source-map.js:1894

现在是时候看了代码了。 Quicksort实现本身在lib / quick-sort.js中生存,从解析代码中调用lib / source-map-famierer.js.Comparison函数中的解析代码,用于排序的CompareByGeneratedPositionSdeflated和CompareByOriginalPositions。

查看这些比较函数的定义以及它们是如何调用的快速排序实现,显示调用站点具有不匹配:

函数compareByOriginalPositions(Mappaa,Mappingb,ocketCompareoriginal){// ...}函数compareBaredBoutedSdeflated(mappapa,mappingb,oyonparegenerated){// ...}函数doquicksort(ary,比较器,p,r){// ...如果(比较器(ARY [J],枢轴)< = 0){// ...} // ...}

通过图书馆来源的Grepping揭示了测试之外的Quicksort Isonly与这两个功能叫做。

diff --git a / dist / source-map.js b / dist / source-map.jsindex ade5bb2..2d39b28 100644 --- a / dist / source-map.js +++ b / dist / source-map。 js @@ -2779,7 +2779,7 @@ return / ****** /(函数(模块){// webpackbootstrap // // *`ary [i + 1 .. j-1中的每个元素]`大于枢轴。对于(var j = p; j ++){ - - if(比较器(Ary [j],枢轴)< = 0){+ if(比较器(Ary [J] ,枢轴,假)< = 0){i + = 1;交换(ary,i,j);}

╭─〜/ src / source-map / bechch [修复比较器调用arity]╰─$ d8 bench-shell-bindings.js解析源mapconsole.iment:reateration,4037.084000console.timent:迭代,4249.258000console .TimeMend:迭代,4241.165000Console.TimeNe:迭代,3936.664000Console.TimeInd:迭代,4131.84400Console.TimeInd:迭代,4140.963000 [统计样本:6,总数:24737 MS,平均:4122.83333333333 MS,STDDEV:132.18789657150916 MS]

只要通过修复arity错配,我们将改进的基准标记在V8上意味着从4774 ms到4123毫秒的14%。如果我们再次配置基准,我们将发现ArgumentsAdTortrampoline已完全从中消失。为什么在第一个地方是呢?

事实证明,ArgumentsAdTortrampoline是V8的COPING COMPACSICT的Variadic Calling约定的机制:您可以调用That函数,其中包含2个参数的3个参数 - 在这种情况下,第三个参数将以未定义的方式填充。 V8通过在堆栈上创建新帧,向下复制参数然后调用目标函数来执行此操作:

虽然冷码可能会忽略不计,但在此代码比较器中,在基准运行期间调用数百万次,该折叠的乘坐适应的开销。

殷勤读者也可能会注意到我们现在显式传递BooleAnvalue False,其中使用了以前使用的未定义。这个Doesteem可以促进绩效改进。如果我们替换虚假的空白0,我们将变得稍微差:

diff --git a / dist / source-map.js b / dist / source-map.jsindex 2d39b28..243b2ef 100644 --- a / dist / source-map.js +++ b / dist / source-map.js @@ -2779,7 +2779,7 @@ return / ****** /(函数(模块){// webpackbootstrap // // *每个`ary [i + 1 .. j-1]`的每个元素大于枢轴。对于(var j = p; j + j ++){ - - if(比较器(ary [j],pivot,false)< = 0){+ if(比较器(ary [j],枢轴,void 0)< = 0){i + = 1;交换(ary,i,j);}

╭─〜/ src / source-map / bechch [修复比较器调用arity]╰─$〜/ src / v8 / v8 / out.gn / x64.release / d8 bench-shell-brientings.js解析源MapConsole.TimeD:迭代,4215.623000Console.Timent:迭代,4247.643000Console.TimeInd:迭代,4425.871000Console.TimeInd:迭代,4167.691000Console.TimeInd:迭代,4343.613000Console.TimeInd:迭代,4209.427000 [统计样本:6,总计:25610 ms,平均:4268.33333333333 ms,STDDEV:106.38947316346669 MS]

对于值得的争论,适应性开销似乎是高度的v8特定。当我基准对抗spidermonkey的改变时,我看不到匹配arity的任何显着性能改善:

╭─〜/ src / source-map / bench [禁用序列化部分的基准测试]╰─$ sm bench-shell-bindings.js解析源地图[统计样品:8,总计:24751 ms,意思是:3093.875 MS,STDDEV:327.27966571700836 MS]╭─〜/ SRC /源 - 地图/ BENCH [FIX比较器调用ARITITY]╰─$ SM BENCH-SHULL-BINDSS.JS解析源地图[STATS样本:8,总计:25397 MS,平均:3174.625 MS,STDDEV:360.4636187025859 MS]

让我们回到排序代码。如果我们再次配置基准测试,我们将从配置文件中获得argumentsadaptortrampoline的argumentsadaptrotrampoline,但CallFunctive_ReceiverisnullorundeDefined仍在那里。这并不奇怪,我们仍在呼叫比较器。

这里的明显选项是尝试并将比较器内环中输入Doquicksort。然而,用不同的比较器功能调用Doquicksort的事实妨碍了内向的方式。

要解决这个问题,我们可以通过克隆它来试图单声道Doquicksort。我们如何做到。

功能SortTemplate(比较器){函数交换(ary,x,y){// ...}函数randonintinrange(低,高){// ...}函数doquicksort(ary,p,r){// 。}返回doquicksort; }

然后,我们可以通过转换SortTemplingInto字符串来生成我们的排序例程的克隆,然后通过FunctionConstructor将其解析回函数:

函数clonesort(比较器){let template = sorttemplate。 toString();让TemplateFn =新函数(“返回$ {模板}`)();返回templatefn(比较器); //调用模板以获取doquicksort}

现在我们可以使用CLONESORT为每个比较器WERE生产分类功能:

让sortcache = new devebap(); //为专门排序缓存。出口。 Quicksort =函数(ary,比较器){让doquicksort = sortcache。得到(比较器); if(doquicksort === void 0){doquicksort = clonesort(比较器); sortcache。设置(比较器,Doquicksort); doquicksort(ary,0,ary。长度 - 1); };

╭─〜/ src / source-map / bechch [每个比较器的克隆分类功能]╰─$ d8 bench-shell-bindings.js解析源mapconsole.imeend:迭代,2955.199000console.imiteNend:迭代, 3084.979000Console.TimeEnd:迭代,3193.134000Console.TimeInd:迭代,3480.459000Console.TimeInd:迭代,3115.011000Console.TimeEnd:迭代,3216.344000Console.TimeInd:迭代,3343.459000Console.TimeInd:迭代,3036.211000 [统计样本:8,总计:25423 MS,平均:3177.875 MS,STDDEV:181.87633161024556女士]

我们可以看到平均时间从4268毫秒到3177毫秒(25%的改进)。

架空符号14.95%* doquicksort:44 11.49%* doquicksort:44 3.29%building:stringequal 3.13%* sourcemapconsumer_parsemappings ../dist/source-map.js:1894 1.86%v8 ::内部:: scringtable :: lookupstringifexists_noallocate 1.86%* sourcemapconsumer_parsemappings ../dist/source-map.js:1894 1.72%buildin:stringcharat 1.67%* sourcemapconsumer_parsemappings ../dist/source-map.js:1894 1.61%v8 ::内部:: scavenger :: scavengeObject 1.45%v8: :内部:(匿名命名空间):: stringtableNoallocateKey :: Ismatch 1.23%内置:StringPrototypesLice 1.17%V8 ::内部:(匿名命名空间):: MakEstringthin 1.08%内置:Keyedloadic_megamorphic 1.05%V8 ::内部:(匿名命名空间):: CopyObjectToObjectElements 0.99%V8 ::内部:: String :: ResidFlat< v8 ::内部:: iterationStringHasher> 0.86%clear_page_c_e 0.77%V8 ::内部:: increstalmarking :: RecordWriteLow 0.48%内置:Mathrandom 0.41%内置:RecordWrite 0.39%内置:Keyedloadic

此时我变得对我们花多少时间达到了Parsingmappings与他们的排序感兴趣。我进入解析代码并添加了几个Date.now()调用:

diff --git a / dist / source-map.js b / dist / source-map.jsindex 75ebbdf..7312058 100644 --- a / dist / source-map.js +++ b / dist / source-map。 js @@ -1906,6 +1906,8,8 @@ return / ****** /(函数(模块){// webpackbootstrap var生成remappings = []; var映射,str,段,结束,值; + + var startparsing = date.now();而(index<长度){if(astr.charat(index)===&#39 ;;'){生成的line ++; @@ -1986,12 +1988, 20 @@ return / ****** /(函数(模块){// webpackbootstrap}}} + var endparsing = date.now(); + var startsortgenerated = date.now(); Quicksort(生成映射,Util。 CompareByGeneratedPositionSdeflated);这个.__ egancemappings = geatuctionmappings; + var nodeoltgenerated = date.now(); + var startsortoriginal = date.now(); quicksort(Oricalinmappings,Util.comPareByBareIginalPositions);这个.__ romationMappings = OricallMappings; + var nodalortoriginal =日期。 now(); ++ console.log(`$ {},$ {DONDORTGENERATED - Startsortgenerated},$ {DINDORTIGINAL - Startsortoriginal}); + CONDOL e.log(`sortgenerated:`); + console.log(`sortoriginal:`); };

╭─〜/ src / source-map / bench [每个比较器的克隆分类功能]╰─$ d8 bench-shell-bindings.js解析源mapparse:1911.846sortgenerated:619.599000000000002sortiginal:905.822000000000001parse:1965.482000000000001排毒:602.193999999995SortiGinal:896.3589999999995 ^ C

以下是在V8和SpiderMOnkey PerbenchMarkmark Markmark Marrow运行中的解析和排序时间如何看出:

在v8中,我们似乎在截然时间的时间消失映射差异。在Spidermonkey解析中相当较快 - 而排序速度较慢。这提示我开始查看解析代码。

架空符号18.23%* doquicksort:44 12.36%* doquicksort:44 3.84%* sourcemapconsumer_parsemappings ../dist/source-map.js:1894 3.07%buildin:stringequal 1.92%v8 ::内部:: scringtable :: lookupstringifexists_noallocate 1.85%* sourcemapconsumer_parsemappings ../dist/source-map.js:1894 1.59%* sourcemapconsumer_parsemappings ../dist/source-map.js:1894 1.54%buildin:stringcharat 1.52%v8 ::内部:(匿名命名空间):: stringtableNoallocateKey: :ismatch 1.38%v8 ::内部:: scavenger :: scavengeObject 1.27%内置:keyedloadic_megamorphic 1.22%buildin:stringprototypeslice 1.10%v8 ::内部:(匿名命名空间):: makestringthin 1.05%v8 :: light :(匿名命名空间):: CopyObjectToObjectElements 1.03%V8 :: Internal :: String :: ResidFlat< v8 ::内部:: iterationStringhasher> 0.88%clear_page_c_c_e 0.51%内置:mathrandom 0.48%内置:Keyedloadic 0.46%V8 :: Internal :: IterationStringHasher :: Hash 0.41%内置:RecordWrite

架空符号3.07%内置:stringequal 1.92%v8 ::内部:: scringtable :: lookupstringifexists_noallocate 1.54%buildin:stringCharat 1.52%V8 ::内部:(匿名命名空间):: stringtableNoallocateKey :: Ismatch 1.38%V8 ::内部:: scavenger :: scaventobject 1.27%内置:keyedloadic_megamorphic 1.22%buildin:stringprototypeslice 1.10%v8 ::内部::(匿名命名空间):: makestringthin 1.05%v8 ::内部:(匿名命名空间):: copyObjecttobojectelements 1.03%V8 ::内部:: string :: scateflat< v8 ::内部:: iterationStringhasher> 0.88%clear_page_c_c_e 0.51%内置:mathrandom 0.48%内置:Keyedloadic 0.46%V8 :: Internal :: IterationStringHasher :: Hash 0.41%内置:RecordWrite

当我开始查看各个条目的呼叫链时,我发现了它们通过keyedloadic_megamorcalphic进入sourcemapconsumer_parsemappings。

- 1.92%v8 ::内部:: scringtable :: lookupstringifexists_noallocate - v8 :: -v8 :: lookupstringifexist_noallocate + 99.80%buildin:keyedloadic_megamorphic-1.52%v8 ::内部:(匿名命名空间):: strytableNoallocateKey :: Ismatch - V8 ::内部:(匿名命名空间):: stringtableNoallocateKey :: Ismatch - 98.32%V8 :: Internal :: stringTable :: lookupstringifexists_noallocate +内置:keyedloadic_megamorphic + 1.68%内置:keyedloadic_megamorphic-1.27%内置:keyedloadic_megamorphic - 内置:keyedloadic_megamorphic + 57.65%* sourcemapconsumer_parsemappings ../dist/source-map.js:1894 + 22.62%* sourcemapconsumer_parsemappings ../dist/source-map.js:1894 + 15.91%* sourcemapconsumer_parsemappings ../dist/source-map.js:1894 + 2.46%内置:解释仪表ristrampoline + 0.61%bytecodehandler:mul + 0.57%* doquicksort:44- 1.10%V8 ::内部:(匿名命名空间):: makestringthin - v8 ::内部:(匿名命名空间):: makestringthin - 94.72%v8 ::内部:: scringtable :: lookupstringi fexists_noallocate +内置:keyedloadic_megamorphic + 3.63%内置:keyedloadic_megamorphic + 1.66%v8 ::内部:: scringtable :: lookupstring

这类调用堆栈向我表示,代码正在执行obj的大量查找[key],其中键是动态构建的字符串。当解析时,我发现了以下代码:

//因为每个偏移相对于前一个偏移编码,//许多段通常具有相同的编码。我们可以通过缓存每个段的解析可变长度字段来利用此//的事实,如果我们再次遇到相同//段,则允许我们避免第二个解析。 for(End =索引;结束<长度; end ++){if(这。_charmakingseed palarator(ast,end)){break; str = Ast。切片(索引,结束);段=缓存[str]; if(段){index + = str。长度 ; } else {segment = [];而(索引< end){base64vlq。解码(Ast,Index,Temp); value = temp。价值 ;索引= temp。休息 ;部分 。推(价值); } // ... cachedsegments [str] =段; }

该代码负责解码Base64 VLQ编码序列,例如,字符串A将被解码为[0],并且uaaaa被解码为[10,0,0,0,0]。 isuggest检查此博客帖子关于介绍internals如果您想了解编码本身禁止。

而不是将每个序列独立解码此代码尝试缓存缓存的段:它向前扫描,直到找到分隔符(或;),如果我们通过查找提取的extuctubstring先前解码的这样的段,则从当前位置提取子字符串。在缓存中 - 如果我们击中缓存,我们将返回缓存的段,否则是解析并缓存缓存中的段。

缓存(AKA Memoization)是一种非常丰富的优化技术 - 但是它只在维护缓存本身时它只有意义,并且查找缓存的结果比再次执行Computation的更便宜。

解析段查看一段段的每个字符。对于每个字符,甚至很少的比较和算术运算来转换Base64特征Into Integer值。然后它将此整数值的数量少量执行若有的位于更大的整数值。然后它将重定值存储到数组中并移动到段的下一部分.Segments限制为5个元素。

要查找缓存的值,我们遍历Segsonce的所有字符,以查找其结束;

我们提取子字符串,这需要分配和潜在驾驶,具体取决于字符串如何在JS VM中实现;

我们将此字符串用作k ......