Вольный перевод статьи Franziska Hinkelmann "Understanding V8’s Bytecode"
Разбираемся с байткодом V8
V8 - это опенсорсный движок для JavaScript, разработанный Google. Chrome, Node.js и многие другие приложения используют V8. В этой статье будет предложено объяснение формата байткода V8, который, на самом деле, станет очень простым для чтения, как только вы поймете некоторые основные концепты.
Ignition, lift-off! Интерпретатор Ignition является частью пайплайна компилятора с 2016-го года.
Когда V8 компилирует JavaScript код, парсер генерирует абстрактное синтаксическое дерево. Синтаксическое дерево - это древовидное представление синтаксической структуры JavaScript кода, из которого интерпретатор Ignition генерирует байткод. TurboFan - это оптимизирующий компилятор, который берет байткод и генерирует из него оптимизированный машинный код.
Пайплайн компилятора V8
Если хотите узнать, для чего нам нужно именно два режима выполнения, можете посмотреть мое видео с конференции JSConfEU:
video: https://www.youtube.com/watch?v=p-iiEDtpy6I&feature=emb_logo
Bytecode является абстракцией машинного кода. Компилировать байткод в машинный код будет проще, если байткод был спроектирован с одинаковой вычислительной моделью, что и физический CPU. Поэтому интерпретаторы обычно являются регистровыми или стэковыми машинами. Ignition - регистровая машина с аккумулятором (регистром процессора).
Байткоды V8 можно рассматривать как маленькие строительные блоки, которые выстраивают любую JavaScript функциональность, когда складываются вместе. V8 имеет несколько сотен байткодов. Есть байткоды для операций, вроде Add и TypeOf, или для загрузки свойств, вроде LdaNamedProperty. V8 также имеет несколько специфичных байткодов, вроде CreateObjectLiteral и SuspendGenerator. Головной файл bytecodes.h определяет полный их список.
Каждый bytecode определяет входные и выходные данные как операнды регистра. Ignition использует геристры r0, r1, r2, ... и аккумулятор, который также используют почти все байткоды. Он похож на обычный регистр, но байткоды его не определяют его точно. Например, Add r1 добавляет значение регистра r1 к значению в аккумуляторе, что позволяет сохранять память и хранить байткоды в более коротком виде.
Множество байткодов начинается с Lda или Sta. a в выражении Ld**a** и St**a** обозначает aккумулятор. Например, LdaSmi [42] подгружает маленькое целое число (Small Integer - Smi) 42 в регистр аккумулятора. Star r0 будет хранить текущее значение регистра r0.
Пора взглянуть на байткод реальной функции.
function incrementX(obj) {
return 1 + obj.x;
}
incrementX({x: 42}); // Компилятор V8 ленив. Если вы не запустите функцию - он ее не интерпретирует.
Если вы хотите посмотреть на байткод V8 в JavaScript коде, то исполните команду с флагом --print-bytecode (для D8 и Node.js версии 8.3 и выше). Для Chrome вам нужно запустить его из командной строки с флагом --js-flags="--print-bytecode", см. Запуск Chromium с флагами.
$ node --print-bytecode incrementX.js
...
[generating bytecode for function: incrementX]
Parameter count 2
Frame size 8
12 E> 0x2ddf8802cf6e @ StackCheck
19 S> 0x2ddf8802cf6f @ LdaSmi [1]
0x2ddf8802cf71 @ Star r0
34 E> 0x2ddf8802cf73 @ LdaNamedProperty a0, [0], [4]
28 E> 0x2ddf8802cf77 @ Add r0, [6]
36 S> 0x2ddf8802cf7a @ Return
Constant pool (size = 1)
0x2ddf8802cf21: [FixedArray] in OldSpace
- map = 0x2ddfb2d02309 <Map(HOLEY_ELEMENTS)>
- length: 1
0: 0x2ddf8db91611 <String[1]: x>
Handler Table (size = 16)
Большую часть вывода можно игнорировать и фокусироваться только на самих байткодах. Ниже описано шаг за шагом, что значит каждый байткод.
LdaSmi [1]
LdaSmi [1] загружает постоянное значение 1 в аккумулятор.
Star r0
Далее, Star r0 в регистре r0 хранит значение, которое на момент находится в аккумуляторе (1).
LdaNamedProperty a0, [0], [4]
LdaNamedProperty загружает именованное свойство a0 в аккумулятор. ai ссылается на i-ый аргумент incrementX(). В этом примере мы ищем именованное свойство в a0, первый аргумент incrementX(). Имя определяется константой 0. LdaNamedProperty использует 0 для нахождения имени в отдельной таблице:
- length: 1
0: 0x2ddf8db91611 <String[1]: x>
Here, 0 отображает значение x, потому этот байткод загружает obj.x.
Для чего используется операнд со значением 4? Это индекс так называемого вектора обратной связи функции incrementX(), которые содержит нужную для оптимизаций производительности рантайм информацию.
Теперь регистры выглядят так:
Add r0, [6]
Последняя инструкция добавляет r0 в аккумулятор, возвращая в результате 43. 6 - это еще один индекс вектора обратной связи.
Return
Return возвращает значение аккумулятора. Это конец функции incrementX(). Вызывающий функцию incrementX() операнд получает значение 43 из аккумулятора и может начать работать с ним.
На первый взгляд, байткод V8 может показаться какой-то загадкой, особенно со всей остальной информацией в выводе. Но как только вы поймете что Ignition это регистровая машина с аккумулятором регистра, вы сможете понять за что отвечает большинство байткодов.
Примечание: в статье описывается байткод V8 версии 6.2, Chrome версии 62 и Node версии 9. Мы постоянно работаем над улучшением потребления памяти и производительности V8, потому детали в других версиях могут отличаться.







Top comments (0)