V8 — это JavaScript-движок Google с открытым кодом. Его используют Chrome, Node.js и многие другие приложения. Этот материал, подготовленный сотрудником Google Франциской Хинкельманн, посвящён описанию формата байт-кода V8. Байт-код довольно просто читать, если понять некоторые базовые вещи.

image

Конвейер компиляции 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-программ?

Комментарии (12)


  1. S_A
    24.08.2017 12:29
    -2

    Уважаемые читатели! Планируете ли вы анализировать байт-код ваших JS-программ?

    Можно было бы сделать голосование в статье.

    Статья очень интересная, но анализировать байт-код я буду вряд ли. Для меня это имело бы смысл, если бы я мог какой-то кусок непосредственно поменять. Есть конечно возможность переписать немного исходного кода, но ведь не зная полностью потроха — далеко не сразу угадаешь же что получится, верно?

    Реально польза могла бы быть, будь конструктор по типу JetBrains MPS в этот байткод. Уже был бы и Typescript -> байткод, и много чего.


  1. Deosis
    24.08.2017 12:31

    V8 имеет несколько сотен байт-кодов.

    Не путайте опкоды и байт-код


    1. fedorro
      24.08.2017 13:13
      +2

      Но так их называют в оригинальной статье, и сами разработчики.


    1. Fesor
      24.08.2017 13:42
      +1

      The name bytecode stems from instruction sets that have one-byte opcodes followed by optional parameters.

      я не понимаю сути притензий.


  1. ReklatsMasters
    24.08.2017 14:50
    +5

    Польза от анализа байткода есть только когда анализируешь деоптимизации. Однако сколько я не вглядывался в вывод --trace-deopt, я ну вообще не могу понять как там вообще что-то оптимизировать. Непонятно откуда функция вызывается, с какими аргументами. Даже брейкпоинт не поставить на деоптимизацию. Максимум на что меня хватает, это на анализ встраивания функций, даже мини-тулзу написал для crankshaft.


  1. nzeemin
    24.08.2017 19:28

    Где-то можно найти информацию о том как в конвейере V8 обрабатывается asm.js и wasm?


    1. khim
      24.08.2017 20:30
      +1

      Для asm.js нет отдельного пайплайна (в отличие от SpiderMonkey), wasm вообще никакого отношения к V8 не имеет.


  1. bano-notit
    25.08.2017 14:08

    Блин, классно конечно это всё, но было бы круче, если бы можно было эти байткоды загружать отдельно. То есть если браузер хром, то он загружает уже готовые байткоды, что и легче и для конечного пользователя практичнее. А остальные пусть загружают исходник...


    Ну это так, идея просто.


    1. Fesor
      25.08.2017 16:14

      1. bano-notit
        25.08.2017 16:33

        Да знаю я про wasm)


        1. Fesor
          25.08.2017 17:35

          Ну так это ж прям то что вы описываете. Еще и не только под хром.


          1. bano-notit
            25.08.2017 17:41

            Это не совсем то, что я описываю. У wasm вся соль на том, что он быстрый, потому что изначально нативный. Ну короче говоря это просто асм. А вот байткод бы дал те же преимущества, что и в яве даёт. То бишь выше по по реализации, ибо байткод изначально умеет работать с дом, он имеет доступ к api всем. Это по сути полнофункциональный js, вот только ниже и не нужно ждать пока браузер его скомпилит в байткод)
            Но это к сожалению относится только к хрому. В лисаньке такое уже не прокатит, у них честный интерпретатор.

            Поэтому я и говорю, что это просто идея, часть работы сделать при билдинге, и клиенту выдавать уже отчасти пережёванный код.