V8 — это JavaScript-движок Google с открытым кодом. Его используют Chrome, Node.js и многие другие приложения. Этот материал, подготовленный сотрудником Google Франциской Хинкельманн, посвящён описанию формата байт-кода V8. Байт-код довольно просто читать, если понять некоторые базовые вещи.
Конвейер компиляции V8
Зажигание! Пуск! Интерпретатор Ignition, название которого можно перевести как «зажигание», является частью конвейера компиляции V8 с 2016-го года
Когда V8 компилирует JavaScript-код, парсер генерирует абстрактное синтаксическое дерево. Синтаксическое дерево — это древовидное представление синтаксической структуры JS-кода. Интерпретатор Ignition генерирует байт-код из этой структуры данных. Оптимизирующий компилятор TurboFan, в итоге, генерирует из байт-кода оптимизированный машинный код.
Конвейер компиляции V8
Если вы хотите узнать о том, почему V8 имеет два режима исполнения, взгляните на моё выступление с JSConfEU.
Основы байт-кода V8
Байт-код — это абстракция машинного кода. Компилировать байт-код в машинный код проще, если байт-код спроектирован с использованием той же вычислительной модели, которая применяется в физическом процессоре. Именно поэтому интерпретаторы часто являются регистровыми или стековыми машинами.
Интерпретатор Ignition — это регистровая машина с накопительным регистром.
Код слева удобен для людей. Код справа — для машин
Байт-коды V8 можно воспринимать как маленькие строительные блоки, которые, собранные вместе, могут реализовать любой функционал JavaScript. V8 имеет несколько сотен байт-кодов. Существуют коды для операторов, вроде Add
или TypeOf
, или для загрузки свойств — вроде LdaNamedProperty
. V8, кроме того, имеет некоторые довольно специфические байт-коды, такие, как CreateObjectLiteral
или SuspendGenerator
. В заголовочном файле bytecodes.h можно найти полный перечень байт-кодов V8.
Каждый байт-код определяет свои входные и выходные данные как регистровые операнды. Ignition использует регистры r0, r1, r2, ...
и накопительный регистр. Почти все байт-коды задействуют накопительный регистр. Он похож на обычный регистр, за исключением того, что его явно не указывают в байт-кодах. Например, команда Add r1
добавляет значение из регистра r1
к тому, что хранится в накопительном регистре. Это делает байт-коды короче и экономит память.
Имена многих байт-кодов начинаются с Lda
или Sta
. Буква a
в Lda
и Sta
является сокращением слова accumulator (накопительный регистр).
Например, команда LdaSmi [42]
загружаем маленькое целое число (Small Integer, Smi) 42
в накопительный регистр. Команда Star r0
записывает значение, которое находится в накопительном регистре, в регистр r0
.
Анализ байт-кода функции
Теперь, после того, как мы разобрали основные понятия, посмотрим на байт-код реальной функции.
function incrementX(obj) {
return 1 + obj.x;
}
incrementX({x: 42}); // Компилятор V8 ленив, поэтому, если вы не вызовете функцию, он не будет её интерпретировать
Если вы хотите увидеть байт-код для JavaScript-кода, вывести его можно, вызвав отладчик D8 или Node.js (начиная с версии 8.3) с флагом --print-bytecode
. В случае с 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
записывает значение, находящееся в накопительном регистре, то есть 1
, в регистр r0
.
LdaNamedProperty a0, [0], [4]
Команда LdaNamedProperty
загружает именованное свойство a0
в накопительный регистр. Конструкция ai
ссылается на i
-й аргумент функции incrementX()
. В этом примере мы обращаемся к именованному свойству по адресу a0
, то есть — к первому аргументу incrementX()
. Имя определяется константой 0
. LdaNamedProperty
использует 0
для поиска имени в отдельной таблице:
- length: 1
0: 0x2ddf8db91611 <String[1]: x>
Здесь 0
отображается на x
. В итоге оказывается, что данный байт-код загружает obj.x
.
Для чего используется операнд с цифрой 4
? Это индекс так называемого вектора обратной связи (feedback vector) функции increment(x)
. Вектор обратной связи содержит информацию времени выполнения, которая используется для оптимизации производительности.
Теперь содержимое регистров выглядит следующим образом.
Add r0, [6]
Последняя инструкция добавляет содержимое r0
к накопительному регистру, что приводит к получению итогового значения 43
. Число 6 —
это ещё один индекс вектора обратной связи.
Return
Команда Return
возвращает содержимое накопительного регистра. Это — завершение функции incrementX()
. То, что вызвало incrementX()
, начинает работу с числом 43
в накопительном регистре и может продолжать выполнять некие действия с этим значением.
Обратите внимание на то, что байт-код, которому посвящён этот материал, используется в V8 версии 6.2, в Chrome 62 и в ещё не выпущенном Node 9. Мы, в Google, постоянно работаем над V8 в направлениях улучшения производительности и уменьшения потребления памяти. В других версиях V8 в байт-коде могут присутствовать некоторые отличия от того, что было описано здесь.
Итоги
На первый взгляд байт-код V8 может показаться довольно-таки загадочным, особенно когда он выводится с массой дополнительных сведений. Однако, как только вы узнаете о том, что Ignition — это регистровая машина с накопительным регистром, вы сможете понять назначение большинства байт-кодов.
Уважаемые читатели! Планируете ли вы анализировать байт-код ваших JS-программ?
Автор: ru_vds