Паттерны оптимизации JavaScript. Часть 2

Andrey Melikhov
devSchacht
Published in
12 min readJul 17, 2017

Перевод статьи Benedikt Meurer: JavaScript Optimization Patterns(Part 2).

Вслед за первой частью этой серии, вышедшей на прошлой неделе, я представляю еще одну (надеюсь, интересную) заметку о паттернах оптимизации JavaScript (основанную на моем четырёхлетнем опыте работы над движком V8). На этой неделе мы рассмотрим оптимизацию под названием специализация контекста функции (function context specialization), которую мы добавили в V8 с запуском TurboFan (другие движки, такие как JavaScriptCore, используют аналогичные оптимизации). Название этой оптимизации немного вводит в заблуждение. В основном это означает, что TurboFan может оборачивать в константы определенные значения при создании оптимизированного кода, и он делает это, специализируясь на сгенерированном машинном коде функции в окружающем её контексте, который V8 отдаёт для представления скоупа (области выполнения) во время выполнения.

Рассмотрим следующий простой фрагмент кода:

const INCREMENT = 1;function incr(x) { return x + INCREMENT; }

Предположим, что мы запускаем это на уровне <script> в Chrome (или на верхнем уровне в оболочке d8), тогда мы получим следующий байткод, сгенерированный для функции incr:

$ out/Release/d8 --print-bytecode ex1.js
...SNIP...
[generating bytecode for function: incr]
Parameter count 2
Frame size 0
35 E> 0x1859bd52f4fe @ 0 : 92 StackCheck
41 S> 0x1859bd52f4ff @ 1 : 13 04 LdaImmutableCurrentContextSlot [4]
52 E> 0x1859bd52f501 @ 3 : 97 00 ThrowReferenceErrorIfHole [0]
50 E> 0x1859bd52f503 @ 5 : 2b 02 03 Add a0, [3]
63 S> 0x1859bd52f506 @ 8 : 96 Return
Constant pool (size = 1)
0x1859bd52f4b1: [FixedArray] in OldSpace
- map = 0x2f062f402309 <Map(PACKED_HOLEY_ELEMENTS)>
- length: 1
0: 0x1859bd52ef11 <String[9]: INCREMENT>
Handler Table (size = 16)
$

Самое интересно здесь — это доступ к константе INCREMENT в скоупе скрипта: она загружается из окружающего контекста через байткод LdaImmutableCurrentContextSlot, а затем сразу же проверяется, является ли её значение тем, что мы называем the_hole в V8. the_hole является внутренним маркером, который используется для реализации временной мертвой зоны для лексического окружения (подробнее в Variables and scoping in ECMAScript 6 Акселя Раушмайера). Это выглядит несколько противоречиво для многих разработчиков, с которыми я общаюсь, поскольку интуитивно мы ожидаем, что виртуальной машине нужно делать меньше работы для const, чем var, особенно внутри локального скоупа, но реальность такова: (по крайней мере изначально) движку необходимо сделать еще больше работы из-за дополнительной проверки TDZ (временной мертвой зоны). Это необходимо из-за того, как работает скоупинг, то есть посмотрим на ex2.js:

console.log(incr(5));const INCREMENT = 1;function incr(x) { return x + INCREMENT; }

И запустим его в оболочке d8:

$ out/Release/d8 ex2.js
ex2.js:5: ReferenceError: INCREMENT is not defined
function incr(x) { return x + INCREMENT; }
^
ReferenceError: INCREMENT is not defined
at incr (ex2.js:5:31)
at ex2.js:1:13
$

Проверка TDZ завершилась неудачно, потому что назначение const INCREMENT = 1 не было выполнено до запуска incr. Я должен признать, что, хотя я уже некоторое время работаю на стороне разработки VM, я все еще считаю это поведение очень противоречивым, но я также не считаю себя очень хорошим дизайнером языка... Хорошо, прочь проповеди. Если снова взглянуть на пример, он, очевидно, будет работать, если вы поместим вызов incr в самый конец

const INCREMENT = 1;function incr(x) { return x + INCREMENT; }console.log(incr(5));

и запустим код в оболочке d8:

$ out/Release/d8 ex3.js
6
$

Так много скрыто в бэгкраунде для временной мертвой зоны (TDZ).

В вопросе производительности здесь есть одно очень интересное (и, возможно, очевидное) наблюдение: после того, как назначается конкретный слот const в контексте, он сохранит это значение и не вернется к тому, чтобы когда-либо снова содержать the_hole (это гарантирует const). И мы используем именно этот факт в TurboFan, чтобы каждый раз не загружать и не проверять значения слота const.

const INCREMENT = 1;function incr(x) { return x + INCREMENT; }// Прогрев
incr(3);
incr(4);
%OptimizeFunctionOnNextCall(incr);
console.log(incr(5));

Мы можем видеть это в оптимизированном машинном коде, который генерирует TurboFan:

$ out/Release/d8 --allow-natives-syntax --print-opt-code --code-comments ex4.js
...SNIP...
-- B0 start (construct frame) --
0x11e35a6041e0 0 55 push rbp
0x11e35a6041e1 1 4889e5 REX.W movq rbp,rsp
0x11e35a6041e4 4 56 push rsi
0x11e35a6041e5 5 57 push rdi
0x11e35a6041e6 6 493ba5680c0000 REX.W cmpq rsp,[r13+0xc68]
0x11e35a6041ed d 0f862a000000 jna 0x11e35a60421d <+0x3d>
-- B2 start --
-- B3 start (deconstruct frame) --
0x11e35a6041f3 13 488b4510 REX.W movq rax,[rbp+0x10]
0x11e35a6041f7 17 a801 test al,0x1
0x11e35a6041f9 19 0f8535000000 jnz 0x11e35a604234 <+0x54>
0x11e35a6041ff 1f 488bd8 REX.W movq rbx,rax
0x11e35a604202 22 48c1eb20 REX.W shrq rbx, 32
0x11e35a604206 26 83c301 addl rbx,0x1
0x11e35a604209 29 0f802a000000 jo 0x11e35a604239 <+0x59>
0x11e35a60420f 2f 48c1e320 REX.W shlq rbx, 32
0x11e35a604213 33 488bc3 REX.W movq rax,rbx
0x11e35a604216 36 488be5 REX.W movq rsp,rbp
0x11e35a604219 39 5d pop rbp
0x11e35a60421a 3a c21000 ret 0x10
...SNIP...
$

Единственная действительно интересная строка здесь — строка с смещением 26 с инструкцией addl rbx, 0x1, где rbx содержит целочисленное значение параметра x, переданного функции (на основе того, что мы прогрели incr целочисленными значениями для x ранее), а 0x1 - зафиксированное значение константы INCREMENT из окружающего контекста. Фиксация константы в этом случае действительна только потому, что TurboFan знает, что никто больше не может изменить значение INCREMENT, как только он уже не является the_hole (то есть вне TDZ). На самом деле это не TurboFan, это интерпретатор Ignition пересылает эту информацию в TurboFan через выделенный байткод LdaImmutableCurrentContextSlot, который мы видели ранее, в частности, это неизменный бит в этом байткоде, который сообщает TurboFan, что контекстный слот больше не может меняться, когда он содержит непустое значение. Мы можем увидеть разницу, если пробуем тот же пример с let:

let INCREMENT = 1;function incr(x) { return x + INCREMENT; }// Прогрев
incr(3);
incr(4);
%OptimizeFunctionOnNextCall(incr);
console.log(incr(5));

Исполнение этого кода ex5.js в оболочке d8 и проверка как байтового кода, так и оптимизированного машинного кода выглядит следующим образом:

$ out/Release/d8 --print-bytecode --allow-natives-syntax --print-opt-code --code-comments ex5.js
...SNIP...
[generating bytecode for function: incr]
Parameter count 2
Frame size 0
33 E> 0xa9399d2f63e @ 0 : 92 StackCheck
39 S> 0xa9399d2f63f @ 1 : 12 04 LdaCurrentContextSlot [4]
50 E> 0xa9399d2f641 @ 3 : 97 00 ThrowReferenceErrorIfHole [0]
48 E> 0xa9399d2f643 @ 5 : 2b 02 03 Add a0, [3]
61 S> 0xa9399d2f646 @ 8 : 96 Return
...SNIP...
-- B0 start (construct frame) --
0x25139be041e0 0 55 push rbp
0x25139be041e1 1 4889e5 REX.W movq rbp,rsp
0x25139be041e4 4 56 push rsi
0x25139be041e5 5 57 push rdi
0x25139be041e6 6 4883ec08 REX.W subq rsp,0x8
0x25139be041ea a 493ba5680c0000 REX.W cmpq rsp,[r13+0xc68]
0x25139be041f1 11 0f864b000000 jna 0x25139be04242 <+0x62>
-- B2 start --
-- B3 start --
0x25139be041f7 17 48b8d1f4d299930a0000 REX.W movq rax,0xa9399d2f4d1 ;; object: 0xa9399d2f4d1 <FixedArray[5]>
0x25139be04201 21 488b402f REX.W movq rax,[rax+0x2f]
0x25139be04205 25 493945a8 REX.W cmpq [r13-0x58],rax
0x25139be04209 29 0f844a000000 jz 0x25139be04259 <+0x79>
-- B4 start (deconstruct frame) --
0x25139be0420f 2f 488b5d10 REX.W movq rbx,[rbp+0x10]
0x25139be04213 33 f6c301 testb rbx,0x1
0x25139be04216 36 0f8564000000 jnz 0x25139be04280 <+0xa0>
0x25139be0421c 3c a801 test al,0x1
0x25139be0421e 3e 0f8561000000 jnz 0x25139be04285 <+0xa5>
0x25139be04224 44 48c1e820 REX.W shrq rax, 32
0x25139be04228 48 488bd3 REX.W movq rdx,rbx
0x25139be0422b 4b 48c1ea20 REX.W shrq rdx, 32
0x25139be0422f 4f 03c2 addl rax,rdx
0x25139be04231 51 0f8053000000 jo 0x25139be0428a <+0xaa>
0x25139be04237 57 48c1e020 REX.W shlq rax, 32
0x25139be0423b 5b 488be5 REX.W movq rsp,rbp
0x25139be0423e 5e 5d pop rbp
0x25139be0423f 5f c21000 ret 0x10
...SNIP...
$

Здесь мы видим, что Ignition должен использовать LdaCurrentContextSlot, то есть он не может доказать, что значение INCREMENT не может измениться впоследствии, потому что любой другой скрипт может просто изменить INCREMENT позже. И поскольку TurboFan не может подставить из константы значение 1 напрямую, он вместо этого должен генерировать явный код для загрузки INCREMENT из контекста скрипта и проверять, что это не the_hole (код между смещением 17 и 2f в приведенном выше списке делает это).

Поэтому в этом смысле const помогает повышению производительности, но только после того, как она достигает оптимизирующего компилятора, и если срабатывает специализация контекста функции, что зависит от довольно простого условия, которое может быть не очевидным: она разрешена только для первого замыкания любой функции в данном нативном контексте (так в V8 называется <iframe>). Так что это значит? В приведенных выше примерах всегда было только одно замыкание incr. Но давайте рассмотрим этот простой пример ex6.js:

const INCREMENT = 1;function makeIncr() {
function incr(x) { return x + INCREMENT; }
return incr;
}
function test(incr) {
// Прогрев
incr(3);
incr(4);
%OptimizeFunctionOnNextCall(incr);
console.log(incr(5));
}
test(makeIncr());
test(makeIncr());

Это определенно немного искусственно, но важно выделить ключевую деталь: теперь есть несколько замыканий для одной и той же функции incr, созданной makeIncr. Выполнение этого в d8 показывает то, что я только что описал:

$ out/Release/d8 --print-bytecode --allow-natives-syntax --print-opt-code --code-comments ex6.js
...SNIP...
[generating bytecode for function: incr]
Parameter count 2
Frame size 0
59 E> 0x34d1b322fb56 @ 0 : 92 StackCheck
65 S> 0x34d1b322fb57 @ 1 : 13 04 LdaImmutableCurrentContextSlot [4]
76 E> 0x34d1b322fb59 @ 3 : 97 00 ThrowReferenceErrorIfHole [0]
74 E> 0x34d1b322fb5b @ 5 : 2b 02 03 Add a0, [3]
87 S> 0x34d1b322fb5e @ 8 : 96 Return
...SNIP...
-- B0 start (construct frame) --
0x30d8696041e0 0 55 push rbp
0x30d8696041e1 1 4889e5 REX.W movq rbp,rsp
0x30d8696041e4 4 56 push rsi
0x30d8696041e5 5 57 push rdi
0x30d8696041e6 6 493ba5680c0000 REX.W cmpq rsp,[r13+0xc68]
0x30d8696041ed d 0f862a000000 jna 0x30d86960421d <+0x3d>
-- B2 start --
-- B3 start (deconstruct frame) --
0x30d8696041f3 13 488b4510 REX.W movq rax,[rbp+0x10]
0x30d8696041f7 17 a801 test al,0x1
0x30d8696041f9 19 0f8535000000 jnz 0x30d869604234 <+0x54>
0x30d8696041ff 1f 488bd8 REX.W movq rbx,rax
0x30d869604202 22 48c1eb20 REX.W shrq rbx, 32
0x30d869604206 26 83c301 addl rbx,0x1
0x30d869604209 29 0f802a000000 jo 0x30d869604239 <+0x59>
0x30d86960420f 2f 48c1e320 REX.W shlq rbx, 32
0x30d869604213 33 488bc3 REX.W movq rax,rbx
0x30d869604216 36 488be5 REX.W movq rsp,rbp
0x30d869604219 39 5d pop rbp
0x30d86960421a 3a c21000 ret 0x10
...SNIP...
-- B0 start (construct frame) --
0x30d8696042c0 0 55 push rbp
0x30d8696042c1 1 4889e5 REX.W movq rbp,rsp
0x30d8696042c4 4 56 push rsi
0x30d8696042c5 5 57 push rdi
0x30d8696042c6 6 4883ec08 REX.W subq rsp,0x8
0x30d8696042ca a 493ba5680c0000 REX.W cmpq rsp,[r13+0xc68]
0x30d8696042d1 11 0f8649000000 jna 0x30d869604320 <+0x60>
-- B2 start --
-- B3 start --
0x30d8696042d7 17 488b45f8 REX.W movq rax,[rbp-0x8]
0x30d8696042db 1b 488b582f REX.W movq rbx,[rax+0x2f]
0x30d8696042df 1f 49395da8 REX.W cmpq [r13-0x58],rbx
0x30d8696042e3 23 0f844e000000 jz 0x30d869604337 <+0x77>
-- B4 start (deconstruct frame) --
0x30d8696042e9 29 488b5510 REX.W movq rdx,[rbp+0x10]
0x30d8696042ed 2d f6c201 testb rdx,0x1
0x30d8696042f0 30 0f8568000000 jnz 0x30d86960435e <+0x9e>
0x30d8696042f6 36 f6c301 testb rbx,0x1
0x30d8696042f9 39 0f8564000000 jnz 0x30d869604363 <+0xa3>
0x30d8696042ff 3f 48c1eb20 REX.W shrq rbx, 32
0x30d869604303 43 488bca REX.W movq rcx,rdx
0x30d869604306 46 48c1e920 REX.W shrq rcx, 32
0x30d86960430a 4a 03d9 addl rbx,rcx
0x30d86960430c 4c 0f8056000000 jo 0x30d869604368 <+0xa8>
0x30d869604312 52 48c1e320 REX.W shlq rbx, 32
0x30d869604316 56 488bc3 REX.W movq rax,rbx
0x30d869604319 59 488be5 REX.W movq rsp,rbp
0x30d86960431c 5c 5d pop rbp
0x30d86960431d 5d c21000 ret 0x10
...SNIP...
$

Ignition использует байткод LdaImmutableCurrentTextSlot, поскольку это слот контекста const, но специализация контекста функции запускается только для первого замыкания. Второе замыкание получает новый оптимизированный код, который не специализирован. Причина этого заключается в том, что если у вас есть несколько замыканий для функции, мы хотели бы выделить общий код для различных замыканий, поскольку создание отдельного объекта кода для каждого замыкания было бы пустой тратой ресурсов (как времени, так и памяти). Если вы используете стрелочные функции с функциями более высокого порядка, например,

let b = a.map(x => x + 1);

вы не хотите, чтобы оптимизирующий компилятор запускался каждый раз, когда вы выполняете эту строку, просто для создания специализированного кодового объекта для x => x + 1. Итак, здесь простое правило: вы получаете специализацию контекста функции только для первого замыкания каждой функции в любом заданном <iframe> (родной контекст в понятиях V8). Нативный контекст не применяется в Node.js, поскольку там у вас есть только один нативный контекст, за исключением случаев использования модуля vm.

Теперь, учитывая, что class подобен let, то есть это изменчивое (мутабельное) связывание (опять же по причинам, которые я не выбирал), вы не обязательно получаете выгоду от специализации контекста функции при использовании классов. Рассмотрим ex7.js:

class A {};function makeA() { return new A; }makeA();
makeA();
%OptimizeFunctionOnNextCall(makeA);
makeA();

Исследуя байткод и оптимизированный код для makeA, мы наблюдаем следующее:

$ out/Release/d8 --print-bytecode --allow-natives-syntax --print-opt-code --code-comments ex7.js
...SNIP...
[generating bytecode for function: makeA]
Parameter count 1
Frame size 8
27 E> 0x1fcce9caf75e @ 0 : 92 StackCheck
32 S> 0x1fcce9caf75f @ 1 : 12 04 LdaCurrentContextSlot [4]
0x1fcce9caf761 @ 3 : 97 00 ThrowReferenceErrorIfHole [0]
0x1fcce9caf763 @ 5 : 1e fa Star r0
39 E> 0x1fcce9caf765 @ 7 : 58 fa fa 00 03 Construct r0, r0-r0, [3]
46 S> 0x1fcce9caf76a @ 12 : 96 Return
...SNIP...
-- B0 start (construct frame) --
0x19518f5041e0 0 55 push rbp
0x19518f5041e1 1 4889e5 REX.W movq rbp,rsp
0x19518f5041e4 4 56 push rsi
0x19518f5041e5 5 57 push rdi
0x19518f5041e6 6 4883ec08 REX.W subq rsp,0x8
0x19518f5041ea a 493ba5680c0000 REX.W cmpq rsp,[r13+0xc68]
0x19518f5041f1 11 0f8673000000 jna 0x19518f50426a <+0x8a>
-- B2 start --
-- B3 start --
0x19518f5041f7 17 48b821f5cae9cc1f0000 REX.W movq rax,0x1fcce9caf521 ;; object: 0x1fcce9caf521 <FixedArray[5]>
0x19518f504201 21 488b402f REX.W movq rax,[rax+0x2f]
0x19518f504205 25 493945a8 REX.W cmpq [r13-0x58],rax
0x19518f504209 29 0f8488000000 jz 0x19518f504297 <+0xb7>
-- B4 start --
0x19518f50420f 2f 48bb29b9e84758150000 REX.W movq rbx,0x155847e8b929 ;; object: 0x155847e8b929 <JSFunction A (sfi = 0x1fcce9caf169)>
0x19518f504219 39 483bd8 REX.W cmpq rbx,rax
0x19518f50421c 3c 0f859c000000 jnz 0x19518f5042be <+0xde>
0x19518f504222 42 498b8578e40300 REX.W movq rax,[r13+0x3e478]
0x19518f504229 49 488d5818 REX.W leaq rbx,[rax+0x18]
0x19518f50422d 4d 49399d80e40300 REX.W cmpq [r13+0x3e480],rbx
0x19518f504234 54 0f864a000000 jna 0x19518f504284 <+0xa4>
-- B6 start --
-- B7 start (deconstruct frame) --
0x19518f50423a 5a 488d5818 REX.W leaq rbx,[rax+0x18]
0x19518f50423e 5e 4883c001 REX.W addq rax,0x1
0x19518f504242 62 49899d78e40300 REX.W movq [r13+0x3e478],rbx
0x19518f504249 69 48bb9105294321300000 REX.W movq rbx,0x302143290591 ;; object: 0x302143290591 <Map(PACKED_HOLEY_ELEMENTS)>
0x19518f504253 73 488958ff REX.W movq [rax-0x1],rbx
0x19518f504257 77 498b5d70 REX.W movq rbx,[r13+0x70]
0x19518f50425b 7b 48895807 REX.W movq [rax+0x7],rbx
0x19518f50425f 7f 4889580f REX.W movq [rax+0xf],rbx
0x19518f504263 83 488be5 REX.W movq rsp,rbp
0x19518f504266 86 5d pop rbp
0x19518f504267 87 c20800 ret 0x8
...SNIP...
$

Это идеальный x64 машинный код для makeA, в этом коде нет лишних проверок (есть две проверки: проверка стека, чтобы гарантировать, что V8 не переполняет исполняемый стек, и выталкивающая проверка указателя, чтобы запускать сборку мусора, когда новое пространство заполнено).

До сих пор единственным способом получить LdaImmutableCurrentContextSlot вместо LdaCurrentContextSlotбыло использование const. Но это было потому, что я демонстрировал только код, работающий на лексически связанных именах на уровне скрипта (или на верхнем уровне в d8). Если мы вернемся к простому примеру let в ex5.js и запустим это в Node 9 (или 8.2.0-rc1), мы увидим, что INCREMENT получает константу, несмотря на использование let:

$ node --print-opt-code --code-comments --allow-natives-syntax ex5.js
...SNIP...
-- B0 start (construct frame) --
0x2f2f61804f60 0 55 push rbp
0x2f2f61804f61 1 4889e5 REX.W movq rbp,rsp
0x2f2f61804f64 4 56 push rsi
0x2f2f61804f65 5 57 push rdi
0x2f2f61804f66 6 493ba5680c0000 REX.W cmpq rsp,[r13+0xc68]
0x2f2f61804f6d d 0f862a000000 jna 0x2f2f61804f9d <+0x3d>
-- B2 start --
-- B3 start (deconstruct frame) --
0x2f2f61804f73 13 488b4510 REX.W movq rax,[rbp+0x10]
0x2f2f61804f77 17 a801 test al,0x1
0x2f2f61804f79 19 0f8535000000 jnz 0x2f2f61804fb4 <+0x54>
0x2f2f61804f7f 1f 488bd8 REX.W movq rbx,rax
0x2f2f61804f82 22 48c1eb20 REX.W shrq rbx, 32
0x2f2f61804f86 26 83c301 addl rbx,0x1
0x2f2f61804f89 29 0f802a000000 jo 0x2f2f61804fb9 <+0x59>
0x2f2f61804f8f 2f 48c1e320 REX.W shlq rbx, 32
0x2f2f61804f93 33 488bc3 REX.W movq rax,rbx
0x2f2f61804f96 36 488be5 REX.W movq rsp,rbp
0x2f2f61804f99 39 5d pop rbp
0x2f2f61804f9a 3a c21000 ret 0x10
-- B4 start (no frame) --
-- B1 start (deferred) --
-- </usr/local/google/home/bmeurer/Projects/v8/ex5.js:3:14> --
0x2f2f61804f9d 3d 48bb40690e0100000000 REX.W movq rbx,0x10e6940
0x2f2f61804fa7 47 33c0 xorl rax,rax
0x2f2f61804fa9 49 488b75f8 REX.W movq rsi,[rbp-0x8]
0x2f2f61804fad 4d e82ef6e7ff call 0x2f2f616845e0 ;; code: STUB, CEntryStub, minor: 8
0x2f2f61804fb2 52 ebbf jmp 0x2f2f61804f73 <+0x13>
0x2f2f61804fb4 54 e847f0cfff call 0x2f2f61504000 ;; deoptimization bailout 0
0x2f2f61804fb9 59 e84cf0cfff call 0x2f2f6150400a ;; deoptimization bailout 1
...SNIP...
$

И аналогично, если мы запускаем ex7.js с биндингом class для A в Node 9 (или 8.2.0-rc1):

$ node --print-opt-code --code-comments --allow-natives-syntax ex7.js
...SNIP...
-- B0 start (construct frame) --
0x2e1f81f84e80 0 55 push rbp
0x2e1f81f84e81 1 4889e5 REX.W movq rbp,rsp
0x2e1f81f84e84 4 56 push rsi
0x2e1f81f84e85 5 57 push rdi
0x2e1f81f84e86 6 493ba5680c0000 REX.W cmpq rsp,[r13+0xc68]
0x2e1f81f84e8d d 0f8648000000 jna 0x2e1f81f84edb <+0x5b>
-- B2 start --
-- B3 start --
0x2e1f81f84e93 13 498b85a8ec0300 REX.W movq rax,[r13+0x3eca8]
0x2e1f81f84e9a 1a 488d5818 REX.W leaq rbx,[rax+0x18]
0x2e1f81f84e9e 1e 49399db0ec0300 REX.W cmpq [r13+0x3ecb0],rbx
0x2e1f81f84ea5 25 0f8647000000 jna 0x2e1f81f84ef2 <+0x72>
-- B5 start --
-- B6 start (deconstruct frame) --
0x2e1f81f84eab 2b 488d5818 REX.W leaq rbx,[rax+0x18]
0x2e1f81f84eaf 2f 4883c001 REX.W addq rax,0x1
0x2e1f81f84eb3 33 49899da8ec0300 REX.W movq [r13+0x3eca8],rbx
0x2e1f81f84eba 3a 48bb012f6b7ceb110000 REX.W movq rbx,0x11eb7c6b2f01 ;; object: 0x11eb7c6b2f01 <Map(PACKED_HOLEY_ELEMENTS)>
0x2e1f81f84ec4 44 488958ff REX.W movq [rax-0x1],rbx
0x2e1f81f84ec8 48 498b5d70 REX.W movq rbx,[r13+0x70]
0x2e1f81f84ecc 4c 48895807 REX.W movq [rax+0x7],rbx
0x2e1f81f84ed0 50 4889580f REX.W movq [rax+0xf],rbx
0x2e1f81f84ed4 54 488be5 REX.W movq rsp,rbp
0x2e1f81f84ed7 57 5d pop rbp
0x2e1f81f84ed8 58 c20800 ret 0x8
...SNIP...
$

Мы видим, что это идеальный код. Причиной этому является модульная система CommonJS, используемая Node. Каждый модуль неявно завернут в функцию. Поэтому ex7.js в Node примерно соответствует следующему коду в Chrome или d8:

(function() {
class A {};
function makeA() { return new A; } makeA();
makeA();
%OptimizeFunctionOnNextCall(makeA);
makeA();
})();

Это упрощено (так как здесь я не хочу объяснять и webpack). Что тут интересно, так это то, что A является локальным для анонимного замыкания, и, таким образом, парсер может фактически доказать, что A никогда не менялся после первоначального определения, потому что никакой код вне замыкания не может видеть (и не прикасаться) к биндингу A. Таким образом, Ignition может использовать LdaImmutableCurrentContextSlot, и TurboFan может генерировать потрясающий код для makeA:

$ out/Release/d8 --print-bytecode --allow-natives-syntax --print-opt-code --code-comments ex9.js
...SNIP...
[generating bytecode for function: makeA]
Parameter count 1
Frame size 8
45 E> 0x22ac28a2f7e6 @ 0 : 92 StackCheck
50 S> 0x22ac28a2f7e7 @ 1 : 13 04 LdaImmutableCurrentContextSlot [4]
0x22ac28a2f7e9 @ 3 : 97 00 ThrowReferenceErrorIfHole [0]
0x22ac28a2f7eb @ 5 : 1e fa Star r0
57 E> 0x22ac28a2f7ed @ 7 : 58 fa fa 00 03 Construct r0, r0-r0, [3]
64 S> 0x22ac28a2f7f2 @ 12 : 96 Return
...SNIP...
-- B0 start (construct frame) --
0x138cd23841e0 0 55 push rbp
0x138cd23841e1 1 4889e5 REX.W movq rbp,rsp
0x138cd23841e4 4 56 push rsi
0x138cd23841e5 5 57 push rdi
0x138cd23841e6 6 493ba5680c0000 REX.W cmpq rsp,[r13+0xc68]
0x138cd23841ed d 0f8648000000 jna 0x138cd238423b <+0x5b>
-- B2 start --
-- B3 start --
0x138cd23841f3 13 498b8578e40300 REX.W movq rax,[r13+0x3e478]
0x138cd23841fa 1a 488d5818 REX.W leaq rbx,[rax+0x18]
0x138cd23841fe 1e 49399d80e40300 REX.W cmpq [r13+0x3e480],rbx
0x138cd2384205 25 0f8647000000 jna 0x138cd2384252 <+0x72>
-- B5 start --
-- B6 start (deconstruct frame) --
0x138cd238420b 2b 488d5818 REX.W leaq rbx,[rax+0x18]
0x138cd238420f 2f 4883c001 REX.W addq rax,0x1
0x138cd2384213 33 49899d78e40300 REX.W movq [r13+0x3e478],rbx
0x138cd238421a 3a 48bb910501aa382d0000 REX.W movq rbx,0x2d38aa010591 ;; object: 0x2d38aa010591 <Map(PACKED_HOLEY_ELEMENTS)>
0x138cd2384224 44 488958ff REX.W movq [rax-0x1],rbx
0x138cd2384228 48 498b5d70 REX.W movq rbx,[r13+0x70]
0x138cd238422c 4c 48895807 REX.W movq [rax+0x7],rbx
0x138cd2384230 50 4889580f REX.W movq [rax+0xf],rbx
0x138cd2384234 54 488be5 REX.W movq rsp,rbp
0x138cd2384237 57 5d pop rbp
0x138cd2384238 58 c20800 ret 0x8
...SNIP...
$

Таким образом, выводы из этого упражнения:

  1. Рассмотрение сгенерированного x64 машинного кода может быть пугающим.
  2. Вместе с сonst вы получаете стоимость проверки TDZ, но это может окупиться в оптимизированном коде.
  3. Использование class эквивалентно использованию let, используйте const, чтобы получить иммутабельную привязку к скоупу сценария.
  4. Виртуальные машины JavaScript пытаются быть умными в скоупе функций (и это используется в Node и Webpack).

Слушайте наш подкаст в iTunes и SoundCloud, читайте нас на Medium, контрибьютьте на GitHub, общайтесь в группе Telegram, следите в Twitter и канале Telegram, рекомендуйте в VK и Facebook.

Статья на GitHub

--

--

Andrey Melikhov
devSchacht

Web-developer in big IT company Перевожу всё, до чего дотянусь. Иногда (но редко) пишу сам.