JSを高速化するためにRustは必芁ないかもしれたせん

数週間前、 「RustずWebAssemblyで゜ヌスマップを酞化する」ずいう投皿を発芋したした。
Twitterで広め、 source-mapラむブラリの通垞のJavaScriptをWebAssemblyでコンパむルされたRustに眮き換えるこずによるパフォヌマンスの向䞊に぀いお説明したす


この投皿は、私がRustやWASMの倧ファンだったからではなく、同様のパフォヌマンスを達成するためにJavascriptに欠けおいる蚀語機胜ず最適化に垞に関心があったために興味をそそりたした。


そこで、GitHubからラむブラリをダりンロヌドし、少しパフォヌマンススタディを行いたした。これに぀いおは、ここでほが逐語的に説明したす。


内容



コヌド怜玢


私の研究では、1月20日のコミット69abb960c97606df99408e6869d66e014aa0fb51のほが暙準のx64.releaseビルドV8を䜿甚しおいたす。 暙準構成ずの私の䞍䞀臎は、必芁に応じお、生成されたマシンコヌドをより深く掘り䞋げるために、GNフラグを介しお逆アセンブラをオンにしたこずだけです。


 ╭─ ~/src/v8/v8 ‹master› ╰─$ gn args out.gn/x64.release --list --short --overrides-only is_debug = false target_cpu = "x64" use_goma = true v8_enable_disassembler = true 

次に、 source-mapモゞュヌルのsource-mapから取埗したした



玔粋なJavaScriptでバヌゞョンをプロファむルしたす


クリヌンなJSバヌゞョンのベンチマヌクを実行するのは簡単でした


 ╭─ ~/src/source-map/bench ‹ c97d38b› ╰─$ d8 bench-shell-bindings.js Parsing source map console.timeEnd: iteration, 4655.638000 console.timeEnd: iteration, 4751.122000 console.timeEnd: iteration, 4820.566000 console.timeEnd: iteration, 4996.942000 console.timeEnd: iteration, 4644.619000 [Stats samples: 5, total: 23868 ms, mean: 4773.6 ms, stddev: 161.22112144505135 ms] 

最初にしたこずは、ベンチマヌクのシリアル化郚分をオフにするこずでした。


 diff --git a/bench/bench-shell-bindings.js b/bench/bench-shell-bindings.js index 811df40..c97d38b 100644 --- a/bench/bench-shell-bindings.js +++ b/bench/bench-shell-bindings.js @@ -19,5 +19,5 @@ load("./bench.js"); print("Parsing source map"); print(benchmarkParseSourceMap()); print(); -print("Serializing source map"); -print(benchmarkSerializeSourceMap()); +// print("Serializing source map"); +// print(benchmarkSerializeSourceMap()); 

次に、これをLinux perfプロファむラヌに投げ蟌みたした。


 ╭─ ~/src/source-map/bench ‹perf-work› ╰─$ perf record -g d8 --perf-basic-prof bench-shell-bindings.js Parsing source map console.timeEnd: iteration, 4984.464000 ^C[ perf record: Woken up 90 times to write data ] [ perf record: Captured and wrote 24.659 MB perf.data (~1077375 samples) ] 

--perf-basic-profフラグをd8バむナリに枡すず、V8がマッピング/tmp/perf-$pid.mapマッピングファむル/tmp/perf-$pid.mapを生成するこずに/tmp/perf-$pid.map 。 このファむルにより、 perf reportはJIT生成のマシンコヌドを理解できたす。


ここに、実行のメむンスレッドを衚瀺した埌のperf report --no-childrenから埗たものを瀺したす。


 Overhead Symbol 17.02% *doQuickSort ../dist/source-map.js:2752 11.20% Builtin:ArgumentsAdaptorTrampoline 7.17% *compareByOriginalPositions ../dist/source-map.js:1024 4.49% Builtin:CallFunction_ReceiverIsNullOrUndefined 3.58% *compareByGeneratedPositionsDeflated ../dist/source-map.js:1063 2.73% *SourceMapConsumer_parseMappings ../dist/source-map.js:1894 2.11% Builtin:StringEqual 1.93% *SourceMapConsumer_parseMappings ../dist/source-map.js:1894 1.66% *doQuickSort ../dist/source-map.js:2752 1.25% v8::internal::StringTable::LookupStringIfExists_NoAllocate 1.22% *SourceMapConsumer_parseMappings ../dist/source-map.js:1894 1.21% Builtin:StringCharAt 1.16% Builtin:Call_ReceiverIsNullOrUndefined 1.14% v8::internal::(anonymous namespace)::StringTableNoAllocateKey::IsMatch 0.90% Builtin:StringPrototypeSlice 0.86% Builtin:KeyedLoadIC_Megamorphic 0.82% v8::internal::(anonymous namespace)::MakeStringThin 0.80% v8::internal::(anonymous namespace)::CopyObjectToObjectElements 0.76% v8::internal::Scavenger::ScavengeObject 0.72% v8::internal::String::VisitFlat<v8::internal::IteratingStringHasher> 0.68% *SourceMapConsumer_parseMappings ../dist/source-map.js:1894 0.64% *doQuickSort ../dist/source-map.js:2752 0.56% v8::internal::IncrementalMarking::RecordWriteSlow 

実際、 「Oxidizing Source Maps ...」で述べたように、このベンチマヌクは基本的に゜ヌトをロヌドしたすdoQuickSort関数はプロファむルの䞊郚に衚瀺され、リストの数倍䞋に衚瀺されたす぀たり、数回最適化および最適化解陀されたした 。


䞊べ替えの最適化-匕数の調敎


プロファむラヌで際立っおいる点の1぀は、疑わしい゚ントリ、぀たりBuiltin:ArgumentsAdaptorTrampolineずBuiltin:CallFunction_ReceiverIsNullOrUndefinedです。これらはV8実装の䞀郚のようです。 perf reportに、それらに぀ながる呌び出しのチェヌンを明らかにするように䟝頌するず、これらの関数は䞻に゜ヌトコヌドから呌び出されるこずがわかりたす。


 - Builtin:ArgumentsAdaptorTrampoline + 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% Builtin:InterpreterEntryTrampoline + 0.55% *doQuickSort ../dist/source-map.js:2752 - Builtin:CallFunction_ReceiverIsNullOrUndefined + 93.88% *doQuickSort ../dist/source-map.js:2752 + 2.24% *SourceMapConsumer_parseMappings ../dist/source-map.js:1894 + 2.01% Builtin:InterpreterEntryTrampoline + 1.49% *SourceMapConsumer_parseMappings ../dist/source-map.js:1894 

そしお、ここでコヌドを芋おみたしょう。 クむック゜ヌトの実装はlib/quick-sort.jsあり、 lib/source-map-consumer.jsのパヌサヌコヌドから呌び出されたす。
゜ヌトに䜿甚される比范関数コンパレヌタは、 compareByGeneratedPositionsDeflatedおよびcompareByOriginalPositionsです。


これらの比范関数の定矩ずクむック゜ヌト実装での呌び出し方法を芋るず、呌び出しの堎所に䞍䞀臎のアリティがあるこずがわかりたす。


 function compareByOriginalPositions(mappingA, mappingB, onlyCompareOriginal) { // ... } function compareByGeneratedPositionsDeflated(mappingA, mappingB, onlyCompareGenerated) { // ... } function doQuickSort(ary, comparator, p, r) { // ... if (comparator(ary[j], pivot) <= 0) { // ... } // ... } 

ラむブラリ゜ヌスをquickSortず、テスト以倖では、これら2぀の関数でのみquickSortが呌び出されおいるこずがquickSortたす。


しかし、アリティを修正したらどうなるでしょうか


 diff --git a/dist/source-map.js b/dist/source-map.js index ade5bb2..2d39b28 100644 --- a/dist/source-map.js +++ b/dist/source-map.js @@ -2779,7 +2779,7 @@ return /* **** */ (function(modules) { // webpackBootstrap // // * Every element in `ary[i+1 .. j-1]` is greater than the pivot. for (var j = p; j < r; j++) { - if (comparator(ary[j], pivot) <= 0) { + if (comparator(ary[j], pivot, false) <= 0) { i += 1; swap(ary, i, j); } 

[泚アセンブリプロセスに時間をかけたくないため、 dist/source-map.js盎接線集したす]


 ╭─ ~/src/source-map/bench ‹perf-work› [Fix comparator invocation arity] ╰─$ d8 bench-shell-bindings.js Parsing source map console.timeEnd: iteration, 4037.084000 console.timeEnd: iteration, 4249.258000 console.timeEnd: iteration, 4241.165000 console.timeEnd: iteration, 3936.664000 console.timeEnd: iteration, 4131.844000 console.timeEnd: iteration, 4140.963000 [Stats samples: 6, total: 24737 ms, mean: 4122.833333333333 ms, stddev: 132.18789657150916 ms] 

アリティの䞍䞀臎を簡単に修正するこずで、ベンチマヌクのV8倀を4774ミリ秒から4123ミリ秒に14改善したした。 ベンチマヌクを再床プロファむルするず、 ArgumentsAdaptorTrampolineがそこから完党に消えおいるこずがわかりたす。 なぜ圌は初めおそこにいたのですか


ArgumentsAdaptorTrampolineは、javascript呌び出しの可倉性をサポヌトするためのV8メカニズムであるこずがわかりたす。2぀だけで3぀の匕数を持぀関数を呌び出すこずができたす。この堎合、3番目の匕数にはundefined倀が入力されたす。 V8は、スタック䞊に新しいフレヌムを䜜成し、そこに匕数をコピヌしお、タヌゲット関数を呌び出すこずでこれを行いたす。


匕数の適応


[ パフォヌマンススタックに぀いお聞いたこずがない堎合は、 りィキペディアずフランシスコヒンケルマンの投皿をご芧ください。]


コヌルドコヌドではこのようなコストは無芖できたすが、ここでは、ベンチマヌクの起動時にcomparatorが䜕癟䞇回も呌び出されたため、匕数を適応させるための远加コストが発生したした。


泚意深い読者は、暗黙のundefined以前に䜿甚された堎所にfalseを明瀺的に枡すこずに気付くかもしれたせん。 これもパフォヌマンスの向䞊に貢献しおいるようです。 falseをvoid 0眮き換えるず、わずかに悪い倀が埗られたす。


 diff --git a/dist/source-map.js b/dist/source-map.js index 2d39b28..243b2ef 100644 --- a/dist/source-map.js +++ b/dist/source-map.js @@ -2779,7 +2779,7 @@ return /* **** */ (function(modules) { // webpackBootstrap // // * Every element in `ary[i+1 .. j-1]` is greater than the pivot. for (var j = p; j < r; j++) { - if (comparator(ary[j], pivot, false) <= 0) { + if (comparator(ary[j], pivot, void 0) <= 0) { i += 1; swap(ary, i, j); } 

 ╭─ ~/src/source-map/bench ‹perf-work U› [Fix comparator invocation arity] ╰─$ ~/src/v8/v8/out.gn/x64.release/d8 bench-shell-bindings.js Parsing source map console.timeEnd: iteration, 4215.623000 console.timeEnd: iteration, 4247.643000 console.timeEnd: iteration, 4425.871000 console.timeEnd: iteration, 4167.691000 console.timeEnd: iteration, 4343.613000 console.timeEnd: iteration, 4209.427000 [Stats samples: 6, total: 25610 ms, mean: 4268.333333333333 ms, stddev: 106.38947316346669 ms] 

そうかもしれないが、議論の適応はV8に非垞に特有のようだ。 SpiderMonkeyのベンチマヌクを開始したずき、䞀臎するアリティからの著しいパフォヌマンスの向䞊は芋られたせんでした。


 ╭─ ~/src/source-map/bench ‹ d052ea4› [Disabled serialization part of the benchmark] ╰─$ sm bench-shell-bindings.js Parsing source map [Stats samples: 8, total: 24751 ms, mean: 3093.875 ms, stddev: 327.27966571700836 ms] ╭─ ~/src/source-map/bench ‹perf-work› [Fix comparator invocation arity] ╰─$ sm bench-shell-bindings.js Parsing source map [Stats samples: 8, total: 25397 ms, mean: 3174.625 ms, stddev: 360.4636187025859 ms] 

[ jsvuツヌルの Matthias Byensのおかげで、SpiderMonkeyシェルのむンストヌルは非垞に簡単になりたした]


゜ヌトコヌドに戻りたしょう。 ベンチマヌクのプロファむルを再床䜜成するず、 ArgumentsAdaptorTrampolineプロファむルから消えたこずがCallFunction_ReceiverIsNullOrUndefinedたすが、 CallFunction_ReceiverIsNullOrUndefinedはただここにありたす。 これは驚くこずではありたせん。なぜなら、私たちはただcomparator呌び出しおいるからです。


䞊べ替えの最適化-単盞化


通垞、䜕が関数を呌び出すよりも速く実行されたすか 圌の䞍圚


ここでの明らかなオプションは、 doQuickSort比范関数を埋め蟌むこずdoQuickSort 。 ただし、 doQuickSortが異なる関数で呌び出されるずいう事実に盎面しおいたす。


これを回避するために、クロヌニングによっおdoQuickSortを単doQuickSortしようずしたす。 方法は次のずおりです。


doQuickSort 、 doQuickSortおよびその他のヘルパヌナヌティリティをSortTemplate関数でラップするこずから始めたしょう。


 function SortTemplate(comparator) { function swap(ary, x, y) { // ... } function randomIntInRange(low, high) { // ... } function doQuickSort(ary, p, r) { // ... } return doQuickSort; } 

次に、 SortTemplateを文字列に倉換し、 Functionコンストラクタヌを介しおFunctionに枡しお解析するこずにより、䞊べ替え手順のクロヌンを䜜成できたす。


 function cloneSort(comparator) { let template = SortTemplate.toString(); let templateFn = new Function(`return ${template}`)(); return templateFn(comparator); // Invoke template to get doQuickSort } 

cloneSortを䜿甚しお、䜿甚する各コンパレヌタの゜ヌト関数を䜜成できたす。


 let sortCache = new WeakMap(); // Cache for specialized sorts. exports.quickSort = function (ary, comparator) { let doQuickSort = sortCache.get(comparator); if (doQuickSort === void 0) { doQuickSort = cloneSort(comparator); sortCache.set(comparator, doQuickSort); } doQuickSort(ary, 0, ary.length - 1); }; 

ベンチマヌクを再起動するず、次のこずがわかりたす。


 ╭─ ~/src/source-map/bench ‹perf-work› [Clone sorting functions for each comparator] ╰─$ d8 bench-shell-bindings.js Parsing source map console.timeEnd: iteration, 2955.199000 console.timeEnd: iteration, 3084.979000 console.timeEnd: iteration, 3193.134000 console.timeEnd: iteration, 3480.459000 console.timeEnd: iteration, 3115.011000 console.timeEnd: iteration, 3216.344000 console.timeEnd: iteration, 3343.459000 console.timeEnd: iteration, 3036.211000 [Stats samples: 8, total: 25423 ms, mean: 3177.875 ms, stddev: 181.87633161024556 ms] 

平均時間が4268ミリ秒から3177ミリ秒に枛少したこずがわかりたす25改善。


プロファむリングは次の図を瀺したす。


  Overhead Symbol 14.95% *doQuickSort :44 11.49% *doQuickSort :44 3.29% Builtin:StringEqual 3.13% *SourceMapConsumer_parseMappings ../dist/source-map.js:1894 1.86% v8::internal::StringTable::LookupStringIfExists_NoAllocate 1.86% *SourceMapConsumer_parseMappings ../dist/source-map.js:1894 1.72% Builtin:StringCharAt 1.67% *SourceMapConsumer_parseMappings ../dist/source-map.js:1894 1.61% v8::internal::Scavenger::ScavengeObject 1.45% v8::internal::(anonymous namespace)::StringTableNoAllocateKey::IsMatch 1.23% Builtin:StringPrototypeSlice 1.17% v8::internal::(anonymous namespace)::MakeStringThin 1.08% Builtin:KeyedLoadIC_Megamorphic 1.05% v8::internal::(anonymous namespace)::CopyObjectToObjectElements 0.99% v8::internal::String::VisitFlat<v8::internal::IteratingStringHasher> 0.86% clear_page_c_e 0.77% v8::internal::IncrementalMarking::RecordWriteSlow 0.48% Builtin:MathRandom 0.41% Builtin:RecordWrite 0.39% Builtin:KeyedLoadIC 

comparator呌び出しに関連するオヌバヌヘッドは、プロファむルから完党に消えたした。


この瞬間、私はマッピングの゜ヌトに比べお、マッピングの解析にどれだけ時間がかかるかに興味を持ちたした。 解析コヌドに行き、 Date.now()いく぀かの呌び出しを远加したした


[ performance.now()を远加したいのですが、SpiderMonkeyシェルはこれをサポヌトしおいたせん。]


 diff --git a/dist/source-map.js b/dist/source-map.js index 75ebbdf..7312058 100644 --- a/dist/source-map.js +++ b/dist/source-map.js @@ -1906,6 +1906,8 @@ return /* **** */ (function(modules) { // webpackBootstrap var generatedMappings = []; var mapping, str, segment, end, value; + + var startParsing = Date.now(); while (index < length) { if (aStr.charAt(index) === ';') { generatedLine++; @@ -1986,12 +1988,20 @@ return /* **** */ (function(modules) { // webpackBootstrap } } } + var endParsing = Date.now(); + var startSortGenerated = Date.now(); quickSort(generatedMappings, util.compareByGeneratedPositionsDeflated); this.__generatedMappings = generatedMappings; + var endSortGenerated = Date.now(); + var startSortOriginal = Date.now(); quickSort(originalMappings, util.compareByOriginalPositions); this.__originalMappings = originalMappings; + var endSortOriginal = Date.now(); + + console.log(`${}, ${endSortGenerated - startSortGenerated}, ${endSortOriginal - startSortOriginal}`); + console.log(`sortGenerated: `); + console.log(`sortOriginal: `); }; 

結果は次のずおりです。


 ╭─ ~/src/source-map/bench ‹perf-work U› [Clone sorting functions for each comparator] ╰─$ d8 bench-shell-bindings.js Parsing source map parse: 1911.846 sortGenerated: 619.5990000000002 sortOriginal: 905.8220000000001 parse: 1965.4820000000004 sortGenerated: 602.1939999999995 sortOriginal: 896.3589999999995 ^C 

したがっお、解析ず゜ヌトの時間は、V8ずSpiderMonkeyでベンチマヌク起動の各反埩に぀いお調べたす。


時間の解析ず゜ヌト


V8では、マッピングの゜ヌトずほが同じ時間をマッピングの分析に費やすようです。 SpiderMonkeyでは、解析ははるかに高速ですが、䞊べ替えが遅くなりたす。 これにより、解析コヌドを確認できたした。


解析の最適化-セグメントキャッシュの削陀


もう䞀床プロファむルを芋おみたしょう。


 Overhead Symbol 18.23% *doQuickSort :44 12.36% *doQuickSort :44 3.84% *SourceMapConsumer_parseMappings ../dist/source-map.js:1894 3.07% Builtin:StringEqual 1.92% v8::internal::StringTable::LookupStringIfExists_NoAllocate 1.85% *SourceMapConsumer_parseMappings ../dist/source-map.js:1894 1.59% *SourceMapConsumer_parseMappings ../dist/source-map.js:1894 1.54% Builtin:StringCharAt 1.52% v8::internal::(anonymous namespace)::StringTableNoAllocateKey::IsMatch 1.38% v8::internal::Scavenger::ScavengeObject 1.27% Builtin:KeyedLoadIC_Megamorphic 1.22% Builtin:StringPrototypeSlice 1.10% v8::internal::(anonymous namespace)::MakeStringThin 1.05% v8::internal::(anonymous namespace)::CopyObjectToObjectElements 1.03% v8::internal::String::VisitFlat<v8::internal::IteratingStringHasher> 0.88% clear_page_c_e 0.51% Builtin:MathRandom 0.48% Builtin:KeyedLoadIC 0.46% v8::internal::IteratingStringHasher::Hash 0.41% Builtin:RecordWrite 

既にわかっおいるJavaScriptコヌドを削陀するず、次のようになりたす。


 Overhead Symbol 3.07% Builtin:StringEqual 1.92% v8::internal::StringTable::LookupStringIfExists_NoAllocate 1.54% Builtin:StringCharAt 1.52% v8::internal::(anonymous namespace)::StringTableNoAllocateKey::IsMatch 1.38% v8::internal::Scavenger::ScavengeObject 1.27% Builtin:KeyedLoadIC_Megamorphic 1.22% Builtin:StringPrototypeSlice 1.10% v8::internal::(anonymous namespace)::MakeStringThin 1.05% v8::internal::(anonymous namespace)::CopyObjectToObjectElements 1.03% v8::internal::String::VisitFlat<v8::internal::IteratingStringHasher> 0.88% clear_page_c_e 0.51% Builtin:MathRandom 0.48% Builtin:KeyedLoadIC 0.46% v8::internal::IteratingStringHasher::Hash 0.41% Builtin:RecordWrite 

個々のレコヌドのコヌルチェヌンを調べ始めたずき、それらの倚くがKeyedLoadIC_Megamorphicを通過するこずがKeyedLoadIC_Megamorphicたした。


 - 1.92% v8::internal::StringTable::LookupStringIfExists_NoAllocate - v8::internal::StringTable::LookupStringIfExists_NoAllocate + 99.80% Builtin:KeyedLoadIC_Megamorphic - 1.52% v8::internal::(anonymous namespace)::StringTableNoAllocateKey::IsMatch - v8::internal::(anonymous namespace)::StringTableNoAllocateKey::IsMatch - 98.32% v8::internal::StringTable::LookupStringIfExists_NoAllocate + Builtin:KeyedLoadIC_Megamorphic + 1.68% Builtin:KeyedLoadIC_Megamorphic - 1.27% Builtin:KeyedLoadIC_Megamorphic - Builtin: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% Builtin:InterpreterEntryTrampoline + 0.61% BytecodeHandler:Mul + 0.57% *doQuickSort :44 - 1.10% v8::internal::(anonymous namespace)::MakeStringThin - v8::internal::(anonymous namespace)::MakeStringThin - 94.72% v8::internal::StringTable::LookupStringIfExists_NoAllocate + Builtin:KeyedLoadIC_Megamorphic + 3.63% Builtin:KeyedLoadIC_Megamorphic + 1.66% v8::internal::StringTable::LookupString 

このような呌び出しスタックの゜ヌトは、コヌドがobj[key]圢匏で倚くのキヌマッピングを実行するこずを通知したした。ここで、 keyは動的に生成された文字列です。 ゜ヌスを芋るず、 次のコヌドが芋぀かりたした。


 // Because each offset is encoded relative to the previous one, // many segments often have the same encoding. We can exploit this // fact by caching the parsed variable length fields of each segment, // allowing us to avoid a second parse if we encounter the same // segment again. for (end = index; end < length; end++) { if (this._charIsMappingSeparator(aStr, end)) { break; } } str = aStr.slice(index, end); segment = cachedSegments[str]; if (segment) { index += str.length; } else { segment = []; while (index < end) { base64VLQ.decode(aStr, index, temp); value = temp.value; index = temp.rest; segment.push(value); } // ... cachedSegments[str] = segment; } 

このコヌドは、Base64 VLQ゚ンコヌドシヌケンスのデコヌドを担圓したす。぀たり、ラむンAは[0]ずしおデコヌドされ、 UAAAA [10,0,0,0,0]デコヌドされたす。 ゚ンコヌドプロセス自䜓をよりよく理解したい堎合は、゜ヌスマップの内郚に぀いおこの投皿を参照するこずをお勧めしたす。


このコヌドは、各シヌケンスを個別にデコヌドする代わりに、埩号化されたセグメントをキャッシュしようずしたす区切り文字 ,たたは; を楜しみ、珟圚の䜍眮から区切り文字たで郚分文字列を抜出し、キャッシュにそのようなセグメントがあるかどうかを確認したす-そしお、ある堎合、キャッシュされたセグメントを返したす。それ以倖の堎合は、解析しおキャッシュに入れたす。


キャッシング メモ化でもありたす は非垞に匷力な最適化手法です。ただし、キャッシュ自䜓を凊理し、そこで結果を芋぀けるこずが、それ自䜓を再蚈算するよりも安䟡である堎合にのみ意味がありたす。


抜象分析


これら2぀の操䜜を抜象的に比范しおみたしょう。


䞀方で、クリヌンな分析


セグメントを解析しお、各文字を1回調べたす。 各文字に぀いお、base64文字を敎数倀に倉換するために、いく぀かの比范および算術挔算が実行されたす。 次に、いく぀かのビット挔算が実行され、これらの数倀が1぀の倧きな数倀に結合されたす。 次に、デコヌドされた倀が配列に栌玍され、セグメントの次の郚分に進みたす。 セグメントは5぀の芁玠に制限されおいたす。


䞀方、キャッシュ


  1. キャッシュされた倀を芋぀けるために、セグメントのすべおの文字を䞀呚しおその終わりを芋぀けたす。
  2. JS VMでの文字列の実装方法に応じお、配眮し、堎合によっおはコピヌする必芁がある郚分文字列を抜出したす。
  3. この行を蟞曞のキヌずしお䜿甚したす
    1. たず、VMがこの文字列のハッシュを蚈算する必芁があり再床枡すず、文字に察しおさたざたなビット単䜍の操䜜を実行したす、文字列を内郚化する必芁がありたす実装によっお異なりたす。
    2. 次に、VMはテヌブルでハッシュマッチングを実行する必芁がありたす。これには、倀に基づいおキヌにアクセスし、他のキヌず比范する必芁がありたす個々の文字を再床衚瀺する必芁がある堎合がありたす。

䞀般に、盎接解析が高速になるように芋えたす。これは、JS VMが個別の算術挔算ずビット挔算でうたく機胜するこずを意味したす。キャッシュにヒットするかどうかを刀断するだけです。


プロファむリングもこれを確認しおいるようです KeyedLoadIC_Megamorphic 、䞊蚘のコヌドのcachedSegments[str]ように、V8でキヌアクセスを実装するために䜿甚されcachedSegments[str] 。


これらの芳察に基づいお、いく぀かの実隓を行いたした。 最初に、解析の終了時にcachedSegmentsキャッシュの倧きさを確認したした。 小さいほど、より効率的なキャッシュが可胜になりたす。


それは非垞に匷く成長するこずがわかりたす


 Object.keys(cachedSegments).length = 155478 

別個のマむクロベンチマヌク


今、私は小さな別個のベンチマヌクを曞くこずにしたした


 //    [n] ,      [v], // ..  0, v, 2*v, ... ,     1, 1 + v, 1 + 2*v, ... // [base]      -    //    // // :   [v],    [cachedSegments] function makeString(n, v, base) { var arr = []; for (var i = 0; i < n; i++) { arr.push([0, base + (i % v), 0, 0].map(base64VLQ.encode).join('')); } return arr.join(';') + ';'; } //   [f]   [str]. function bench(f, str) { for (var i = 0; i < 1000; i++) { f(str); } } //     [f]  [str]. //    [v]   function measure(v, str, f) { var start = Date.now(); bench(f, str); var end = Date.now(); report(`${v}, ${f.name}, ${(end - start).toFixed(2)}`); } async function measureAll() { for (let v = 1; v <= 256; v *= 2) { //    1000     [v] , //   [cachedSegments]   [v]  . let str = makeString(1000, v, 1024 * 1024); let arr = encoder.encode(str); //  10     . for (var j = 0; j < 10; j++) { measure(j, i, str, decodeCached); measure(j, i, str, decodeNoCaching); measure(j, i, str, decodeNoCachingNoStrings); measure(j, i, arr, decodeNoCachingNoStringsPreEncoded); await nextTick(); } } } function nextTick() { return new Promise((resolve) => setTimeout(resolve)); } 

Base64 VLQ, .


— decodeCached , source-map — :


 function decodeCached(aStr) { var length = aStr.length; var cachedSegments = {}; var end, str, segment, value, temp = {value: 0, rest: 0}; const decode = base64VLQ.decode; var index = 0; while (index < length) { // Because each offset is encoded relative to the previous one, // many segments often have the same encoding. We can exploit this // fact by caching the parsed variable length fields of each segment, // allowing us to avoid a second parse if we encounter the same // segment again. for (end = index; end < length; end++) { if (_charIsMappingSeparator(aStr, end)) { break; } } str = aStr.slice(index, end); segment = cachedSegments[str]; if (segment) { index += str.length; } else { segment = []; while (index < end) { decode(aStr, index, temp); value = temp.value; index = temp.rest; segment.push(value); } if (segment.length === 2) { throw new Error('Found a source, but no line and column'); } if (segment.length === 3) { throw new Error('Found a source and line, but no column'); } cachedSegments[str] = segment; } index++; } } 

— decodeNoCaching . , decodeCached , . . Array Int32Array segment .


 function decodeNoCaching(aStr) { var length = aStr.length; var cachedSegments = {}; var end, str, segment, temp = {value: 0, rest: 0}; const decode = base64VLQ.decode; var index = 0, value; var segment = new Int32Array(5); var segmentLength = 0; while (index < length) { segmentLength = 0; while (!_charIsMappingSeparator(aStr, index)) { decode(aStr, index, temp); value = temp.value; index = temp.rest; if (segmentLength >= 5) throw new Error('Too many segments'); segment[segmentLength++] = value; } if (segmentLength === 2) { throw new Error('Found a source, but no line and column'); } if (segmentLength === 3) { throw new Error('Found a source and line, but no column'); } index++; } } 

, , decodeNoCachingNoString JavaScript- , utf8- Uint8Array . , , , JS VM . String.prototype.charCodeAt , , JS VM.


: utf8 , . "" , , typed array ⇒ string ⇒ typed array . , source map array buffer , .


 let encoder = new TextEncoder(); function decodeNoCachingNoString(aStr) { decodeNoCachingNoStringPreEncoded(encoder.encode(aStr)); } function decodeNoCachingNoStringPreEncoded(arr) { var length = arr.length; var cachedSegments = {}; var end, str, segment, temp = {value: 0, rest: 0}; const decode2 = base64VLQ.decode2; var index = 0, value; var segment = new Int32Array(5); var segmentLength = 0; while (index < length) { segmentLength = 0; while (arr[index] != 59 && arr[index] != 44) { decode2(arr, index, temp); value = temp.value; index = temp.rest; if (segmentLength < 5) { segment[segmentLength++] = value; } } if (segmentLength === 2) { throw new Error('Found a source, but no line and column'); } if (segmentLength === 3) { throw new Error('Found a source and line, but no column'); } index++; } } 

, Chrome Dev
66.0.3343.3 (V8 6.6.189 ) Firefox Nightly 60.0a1 (2018-02-11) :


Different Decodes


:



, V8 - charCodeAt – , Crankshaft charCodeAt , charCodeAt , , , .


- V8 :



2018 , , charCodeAt . Chrome Beta Chrome Dev.


Different Decodes


, V8 : charCodeAt 6.5.254.21 6.6.189 . "no cache" "using array", , charCodeAt V8 , Uint8Array , . , V8.


, , . なぜそう , V8:


 function foo(str, i) { return str.charCodeAt(i); } let str = "fisk"; foo(str, 0); foo(str, 0); foo(str, 0); %OptimizeFunctionOnNextCall(foo); foo(str, 0); 

 ╭─ ~/src/v8/v8 ‹master› ╰─$ out.gn/x64.release/d8 --allow-natives-syntax --print-opt-code --code-comments x.js 

, , V8 charCodeAt . , , V8, , charCodeAt .



, source-map .


Parse and Sort times


, , : .


–


, .


:


  1. originalMappings compareByOriginalPositions ;
  2. generatedMappings compareByGeneratedPositionsDeflated .

originalMappings


compareByOriginalPositions .


 function compareByOriginalPositions(mappingA, mappingB, onlyCompareOriginal) { var cmp = strcmp(mappingA.source, mappingB.source); if (cmp !== 0) { return cmp; } cmp = mappingA.originalLine - mappingB.originalLine; if (cmp !== 0) { return cmp; } cmp = mappingA.originalColumn - mappingB.originalColumn; if (cmp !== 0 || onlyCompareOriginal) { return cmp; } cmp = mappingA.generatedColumn - mappingB.generatedColumn; if (cmp !== 0) { return cmp; } cmp = mappingA.generatedLine - mappingB.generatedLine; if (cmp !== 0) { return cmp; } return strcmp(mappingA.name, mappingB.name); } 

, source , . source , . , originalMappings , , originalMappings : originalMappings[i] i . , originalMappings[i] , , .


[ , – ]


:


 if (typeof mapping.originalLine === 'number') { //      originalMappings.push(mapping). //            . let currentSource = mapping.source; while (originalMappings.length <= currentSource) { originalMappings.push(null); } if (originalMappings[currentSource] === null) { originalMappings[currentSource] = []; } originalMappings[currentSource].push(mapping); } 

:


 var startSortOriginal = Date.now(); //    : // quickSort(originalMappings, util.compareByOriginalPositions); for (var i = 0; i < originalMappings.length; i++) { if (originalMappings[i] != null) { quickSort(originalMappings[i], util.compareByOriginalPositionsNoSource); } } var endSortOriginal = Date.now(); 

compareByOriginalPositionsNoSource compareByOriginalPositions , source — , originalMappings[i] .


Parse and Sort times


V8, SpiderMonkey, V8.


[ originalMappings : originalMappings , originalMappings[i] . , .]


generatedMappings


generatedMappings compareByGeneratedPositionsDeflated .


 function compareByGeneratedPositionsDeflated(mappingA, mappingB, onlyCompareGenerated) { var cmp = mappingA.generatedLine - mappingB.generatedLine; if (cmp !== 0) { return cmp; } cmp = mappingA.generatedColumn - mappingB.generatedColumn; if (cmp !== 0 || onlyCompareGenerated) { return cmp; } cmp = strcmp(mappingA.source, mappingB.source); if (cmp !== 0) { return cmp; } cmp = mappingA.originalLine - mappingB.originalLine; if (cmp !== 0) { return cmp; } cmp = mappingA.originalColumn - mappingB.originalColumn; if (cmp !== 0) { return cmp; } return strcmp(mappingA.name, mappingB.name); } 

generatedLine . , , , , generatedMappings .


, :


 while (index < length) { if (aStr.charAt(index) === ';') { generatedLine++; // ... } else if (aStr.charAt(index) === ',') { // ... } else { mapping = new Mapping(); mapping.generatedLine = generatedLine; // ... } } 

generatedLine , , generatedLine , , generatedMappings generatedLine , . . :


 let subarrayStart = 0; while (index < length) { if (aStr.charAt(index) === ';') { generatedLine++; // ... //   [subarrayStart, generatedMappings.length]. sortGenerated(generatedMappings, subarrayStart); subarrayStart = generatedMappings.length; } else if (aStr.charAt(index) === ',') { // ... } else { mapping = new Mapping(); mapping.generatedLine = generatedLine; // ... } } //    sortGenerated(generatedMappings, subarrayStart); 

, , VM Array.prototype.sort .


[: , 
 , . , generatedMappings , , generatedMappings , .]


 const compareGenerated = util.compareByGeneratedPositionsDeflatedNoLine; function sortGenerated(array, start) { let l = array.length; let n = array.length - start; if (n <= 1) { return; } else if (n == 2) { let a = array[start]; let b = array[start + 1]; if (compareGenerated(a, b) > 0) { array[start] = b; array[start + 1] = a; } } else if (n < 20) { for (let i = start; i < l; i++) { for (let j = i; j > start; j--) { let a = array[j - 1]; let b = array[j]; if (compareGenerated(a, b) <= 0) { break; } array[j - 1] = b; array[j] = a; } } } else { quickSort(array, compareGenerated, start); } } 

:


Parse and Sort times


, – , generatedMappings , . ( )



Parse and Sort times


, .


- , ?


, : asm.js / WASM, Rust JavaScript .


– GC


Mapping , (GC) – , – . .


[ , JavaScript- , . , , , , : , C++ :-(]


Mapping , , .


 function Mapping(memory) { this._memory = memory; this.pointer = 0; } Mapping.prototype = { get generatedLine () { return this._memory[this.pointer + 0]; }, get generatedColumn () { return this._memory[this.pointer + 1]; }, get source () { return this._memory[this.pointer + 2]; }, get originalLine () { return this._memory[this.pointer + 3]; }, get originalColumn () { return this._memory[this.pointer + 4]; }, get name () { return this._memory[this.pointer + 5]; }, set generatedLine (value) { this._memory[this.pointer + 0] = value; }, set generatedColumn (value) { this._memory[this.pointer + 1] = value; }, set source (value) { this._memory[this.pointer + 2] = value; }, set originalLine (value) { this._memory[this.pointer + 3] = value; }, set originalColumn (value) { this._memory[this.pointer + 4] = value; }, set name (value) { this._memory[this.pointer + 5] = value; }, }; 

, :


 BasicSourceMapConsumer.prototype._parseMappings = function (aStr, aSourceRoot) { //  4    .     //  aStr        this._memory = new Int32Array(1 * 1024 * 1024); this._allocationFinger = 0; let mapping = new Mapping(this._memory); // ... while (index < length) { if (aStr.charAt(index) === ';') { //  ,        , //         sortGenerated(this._memory, generatedMappings, previousGeneratedLineStart); } else { this._allocateMapping(mapping); // ... //     ""   . generatedMappings.push(mapping.pointer); if (segmentLength > 1) { // ... originalMappings[currentSource].push(mapping.pointer); } } } // ... for (var i = 0; i < originalMappings.length; i++) { if (originalMappings[i] != null) { quickSort(this._memory, originalMappings[i], util.compareByOriginalPositionsNoSource); } } }; BasicSourceMapConsumer.prototype._allocateMapping = function (mapping) { let start = this._allocationFinger; let end = start + 6; if (end > this._memory.length) { // Do we need to grow memory buffer? let memory = new Int32Array(this._memory.length * 2); memory.set(this._memory); this._memory = memory; } this._allocationFinger = end; let memory = this._memory; mapping._memory = memory; mapping.pointer = start; mapping.name = 0x7fffffff; // Instead of null use INT32_MAX. mapping.source = 0x7fffffff; // Instead of null use INT32_MAX. }; exports.compareByOriginalPositionsNoSource = function (memory, mappingA, mappingB, onlyCompareOriginal) { var cmp = memory[mappingA + 3] - memory[mappingB + 3]; // originalLine if (cmp !== 0) { return cmp; } cmp = memory[mappingA + 4] - memory[mappingB + 4]; // originalColumn if (cmp !== 0 || onlyCompareOriginal) { return cmp; } cmp = memory[mappingA + 1] - memory[mappingB + 1]; // generatedColumn if (cmp !== 0) { return cmp; } cmp = memory[mappingA + 0] - memory[mappingB + 0]; // generatedLine if (cmp !== 0) { return cmp; } return memory[mappingA + 5] - memory[mappingB + 5]; // name }; 

, . Mapping , , . VM allocation sinking , scalar replacement . , SpiderMonkey , .


[ JS. , , "" source-map , WASM]


, GC :


After reworking allocation


After reworking allocation


, SpiderMonkey , , .


SpiderMonkey


, SpiderMonkey: 4 64 , , 7 .


After reworking allocation


- , , .


SpiderMonkey Jan de Mooij , – asm.js 2012 
 SpiderMonkey, .


– Uint8Array .


, Uint8Array , .


After reworking allocation


[ source-map , JavaScript- JSON.decode . , .]



:


 $ d8 bench-shell-bindings.js ... [Stats samples: 5, total: 24050 ms, mean: 4810 ms, stddev: 155.91063145276527 ms] $ sm bench-shell-bindings.js ... [Stats samples: 7, total: 22925 ms, mean: 3275 ms, stddev: 269.5999093306804 ms] 


 $ d8 bench-shell-bindings.js ... [Stats samples: 22, total: 25158 ms, mean: 1143.5454545454545 ms, stddev: 16.59358125226469 ms] $ sm bench-shell-bindings.js ... [Stats samples: 31, total: 25247 ms, mean: 814.4193548387096 ms, stddev: 5.591064299397745 ms] 

After reworking allocation


After reworking allocation


4 !


, , originalMappings , . , originalMappings :



, allGeneratedPositionsFor originalMappings[i] , , - .


, V8 19 , V8 19 ( untrusted code mitigations ).


After reworking allocation


"" source-map


19 , source-map source-map , Rust WASM.


Rust parse_mappings , Rust- , generatedMappings . JS-, originalMappings[i] .


( generatedMappings ) generatedMappings .


Parse only times


Parse and iterate times


, , Rust- generatedMappings , JS-.


" Rust+WASM " . , , Rust source-map .


(27-02-2018)


, source-map Rust+WASM, . :


Parse and iterate times


, WASM+Rust 15% SpiderMonkey V8.



JavaScript


–


– . . perf – "" , .


. , .



– . , 100 , 3333 30 ?


100000log⁡1000003 3333×30log⁡30– , .


, : , , ?


VM . !


. . : " !" 。 (VM) – , , . , . C++ .


VM


, JavaScript.


, , , - .


/



, , , .


JavaScript , , , VM, – .


VM , . , DevTools.



, , , . ? , . , . , (.. Rust).


: - , , , . , , .


あずがき


, :


  1. ;
  2. , , ;
  3. , V8.

, , . , "" , "" , . :



. V8 . JS . . (Rust, ) .


.


, , ( ) . . JS VM .



 ? , , , . , .


明らかに、各開発者ず各チヌムはN、JavaScriptコヌドを慎重にプロファむリング、読み取り、怜蚎する時間を費やすかM、ファヌムを蚀語に曞き換える時間を費やすかを自由に遞択できたすX。


ただし、a遞択肢が実際に存圚するこずを誰もが完党に認識する必芁がありたす。b蚀語デザむナヌずアヌティストが協力しお、この遞択をたすたす明確にしなくおはなりたせん。぀たり、蚀語機胜ずツヌルに取り組み、「グルヌプ3」最適化の必芁性を枛らしたす。



Source: https://habr.com/ru/post/J350018/


All Articles