【JSConf EU 2018】JavaScript Engine: лучшая часть

внешний интерфейс переводчик JavaScript V8

Успешно завершилась конференция JSConf EU 2018. Матиас Байненс, разработчики Google V8, и Бенедикт Мёрер выступили с докладом «JavaScript Engines: The Good Parts™». Эта статья познакомит вас с ключевыми моментами, упомянутыми в выступлении.

Лекция, часть 1: Движок JavaScript

JavaScript-движок

Механизм JavaScript анализирует исходный код и преобразует его в абстрактное синтаксическое дерево (AST). На основе AST интерпретатор создает байт-код. В этот момент движок выполняет код JavaScript. Чтобы ускорить процесс, байт-код отправляется компилятору вместе с данными профилирования. Компилятор делает определенные предположения на основе доступных данных профилирования, а затем генерирует оптимизированный машинный код.

1

Интерпретатор/компилятор в движке JavaScript

Объясните, как движки JavaScript выполняют ваш код, сравнив некоторые различия в реализации основных движков JavaScript.

Интерпретатор быстро создает неоптимизированный байт-код, компилятор занимает больше времени, но в итоге создает высокооптимизированный машинный код.

2
Вышеизложенное в основном представляет собой рабочий процесс V8 в Chrome и Node.js.

3
Интерпретатор V8 отвечает за генерацию и выполнение байт-кода. Когда он запускает байт-код, он собирает данные профилирования, которые являются основой для оптимизации. Когда функция запускается, сгенерированный байт-код и данные профилирования передаются компилятору TurboFan, который генерирует оптимизированный машинный код на основе данных профилирования.

4
SpiderMonkey — это движок JavaScript от Mozilla, используемый в Firefox и SpiderNode, и он немного отличается от описанного выше процесса. У него два компилятора. Компилятор Baseline генерирует некоторый оптимизированный код. В сочетании с данными профилирования, собранными во время выполнения кода, компилятор IonMonkey может создавать сильно оптимизированный код. Если оптимизация не удалась, IonMonkey возвращается к оптимизированному коду Baseline.

5
Chakra, JavaScript-движок Microsoft для Edge и Node-ChakraCore, имеет два очень похожих оптимизирующих компилятора. Байт-код, сгенерированный интерпретатором, сначала генерирует оптимизированный код с помощью SimuleJIT, где JIT означает JIT-компилятор. В сочетании с данными профилирования FuljJIT может генерировать более оптимизированный код.

6
JavaScriptCore (сокращенно JSC), движок Apple JavaScript для Safari и React Native, включает три разных компилятора. Интерпретатор LLInt генерирует байт-код, а компилятор Baseline может генерировать оптимизированный код. Он также может быть дополнительно оптимизирован компилятором DFG и, наконец, передан компилятору FTL для оптимизации.

Интерпретаторы могут быстро генерировать байт-код, но выполнение байт-кода обычно не очень эффективно. Компиляторы, с другой стороны, занимают больше времени, но в итоге создают более эффективный машинный код. Существует компромисс между быстрым выполнением кода (интерпретатор) и увеличением времени выполнения кода с оптимальной производительностью (компилятор).

Лекция, часть 2: Объектная модель JavaScript

Спецификация ECMAScript в основном определяет все объекты как словари и сопоставляет строковые ключи с объектами описания.

7

JavaScript определяет массив как объект. Например, все ключи, включая индексы массива, явно представлены в виде строк. Первый элемент массива хранится по ключу "0".

8
Свойство «длина» — это еще одно неперечислимое и ненастраиваемое свойство. Как только элемент добавлен в массив, JavaScript автоматически обновляет объект описания [[Value] свойства «length».
9

Лекция, часть 3: Оптимизация доступа к атрибутам

Доступ к свойствам — наиболее распространенная операция в программах на JavaScript. Быстрый доступ к свойствам имеет решающее значение для движков JavaScript.

const object = {
	foo: 'bar',
	baz: 'qux',
};

// Here, we’re accessing the property `foo` on `object`:
doSomething(object.foo);
//          ^^^^^^^^^^

Shape

В программах на JavaScript часто встречаются объекты с одинаковыми ключами свойств. Такие объекты имеют одинаковую форму.

const object1 = { x: 1, y: 2 };
const object2 = { x: 3, y: 4 };
// `object1` and `object2` have the same shape.`
在相同Shape的对象上访问相同的属性也是非常常见的:
`function logX(object) {
	console.log(object.x);
	//          ^^^^^^^^
}

const object1 = { x: 1, y: 2 };
const object2 = { x: 3, y: 4 };

logX(object1);
logX(object2);

Таким образом, механизм JavaScript может оптимизировать доступ к свойствам на основе формы объекта.

Предположим, у нас есть объект со свойствами x и y, который использует структуру данных словаря, которую мы обсуждали ранее: он содержит ключи в виде строк, и они указывают на объекты описания для своих соответствующих свойств.

10
Если вы обращаетесь к такому свойству, как object.y, механизм JavaScript будет искать ключевое слово «y» в объекте js, затем загружать соответствующий объект описания и, наконец, возвращать значение свойства [[Value]].

Если каждый объект JS хранит объект описания, это приведет к большому дублированию и ненужным затратам памяти. Механизм JavaScript хранит формы этих объектов отдельно.

11
Этот Shape использует смещение вместо [[Value]], и каждый объект JS с одним и тем же Shape указывает на этот экземпляр Shape.

12
Когда есть несколько объектов, если они имеют одинаковую форму, нужно сохранить только один!

Все движки JavaScript используют Shape в качестве оптимизации, но не все называют его Shape:

  • Академические документы называют это скрытыми классами.
  • V8 называет это Картами
  • Чакра называет это типами
  • JavaScriptCore называет это структурами
  • SpiderMonkey называет это Shapes Форма используется в речи единообразно.

Цепочки переходов и деревья переходов

Если объект указывает на фигуру и вы добавляете к нему новое свойство, как движок JavaScript найдет новую фигуру. Формы этого типа формируют так называемую «цепочку переходов» в движке JavaScript. Ниже приведен пример:

13
Объект начинается без свойств, поэтому он указывает на пустую фигуру. Следующий оператор присваивает этому объекту свойство со значением 5 и ключом «x», поэтому механизм JavaScript указывает объекту JS объект Shape, который содержит свойство «x», и добавляет 5 к 0-й позиции объекта. JS-объект. Следующая строка кода добавляет свойство «y», поэтому движок указывает объект JS на другую форму со свойствами «x» и свойством «y» и добавляет 6 к биту 1 объекта JS.

Нам даже не нужно хранить полную таблицу атрибутов для каждой фигуры. Вместо этого каждая форма должна знать только о новых свойствах, которые она вводит. Например, в этом случае нам не нужно хранить информацию о «x» в последней фигуре, потому что ее можно найти раньше в цепочке. Для этого каждая фигура связана с предыдущей фигурой:

14
Если вы пишете o.x в коде JavaScript, механизм JavaScript находит форму, которая вводит свойство «y» через цепочку переходов, и, таким образом, находит свойство «x».

Но что, если нет возможности создать цепочку переходов? Например, что, если у вас есть два пустых объекта и к каждому добавлены разные свойства?

const object1 = {};
object1.x = 5;
const object2 = {};
object2.y = 6;

В этом случае мы должны заменить цепочку ветвью, и в итоге мы получим дерево переходов:

15

Движок применяет некоторые оптимизации к объектам, которые уже содержат свойства. Либо начните с пустого объекта и добавьте «x», либо создайте объект, который уже содержит «x»:

const object1 = {};
object1.x = 5;
const object2 = { x: 6 };

16
Объект указывает на фигуру, содержащую свойство «x» в самом начале, эффективно пропуская пустые фигуры. V8 и SpiderMonkey делают именно это. Эта оптимизация сокращает цепочку переходов и делает создание объектов из литералов более эффективным.

Внутреннее поведение (ICS)

ICs являются ключевыми факторами JavaScript для быстрой работы! Механизм JavaScript для поиска информации с использованием интегральных схем, чтобы запомнить, где находятся объекты, чтобы уменьшить количество поисков. Вот функция getX, она загружается из объекта и атрибута "x":

function getX(o) {
	return o.x;
}

Если мы запустим эту функцию в АО, она сгенерирует следующий байт-код:

17
Первая инструкция get_by_id загружает атрибут "x" из первого аргумента (arg1) и сохраняет результат в loc0. Вторая инструкция возвращает LoC0, в который мы сохранили.

Кроме того, в инструкцию get_by_id встраивается встроенный кэш, который состоит из двух неинициализированных слотов.

18
Теперь предположим, что мы вызываем getX с параметром {x:"a"}. Как мы знаем, этот объект указывает на Shape со свойством «x», и этот Shape хранит смещение свойства «x» и объект описания. Когда функция выполняется впервые, инструкция get_by_id ищет атрибут «x» и обнаруживает, что значение хранится по смещению 0.
19
IC, встроенный в инструкцию get_by_id, запоминает, из какой формы и смещения был найден атрибут:
20
Для последующих запусков IC просто нужно сравнить форму, и если она такая же, как и раньше, просто загрузить значение из сохраненного смещения. В частности, если движок JavaScript видит, что объект указывает на Shape, ранее записанный IC, нет необходимости в повторном поиске, и дорогостоящий поиск свойств можно полностью пропустить. Это намного быстрее, чем искать недвижимость каждый раз.

Лекция, часть 4: Эффективные массивы хранения

Массивы используют индексы массива для хранения свойств. Значения этих свойств называются элементами массива. Неразумно хранить объекты описания для каждого элемента массива. Индексированные свойства массива по умолчанию доступны для записи, перечисления и настройки, а механизм JavaScript хранит элементы массива отдельно от других свойств.

Взгляните на этот массив:

const array = [
	'#jsconfeu',
];

Движок хранит массив длины 1 и указывает на фигуру, содержащую длину, со смещением 0.

21

22
Каждый массив имеет отдельное резервное хранилище элементов, содержащее значения свойств для всех индексов массива. Механизмы JavaScript не должны хранить какие-либо объекты описания для каждого элемента массива, поскольку обычно все они доступны для записи, перечисления и настройки.

Что если я изменю объект описания элемента массива?

// Please don’t ever do this!
const array = Object.defineProperty(
	[],
	'0',
	{
		value: 'Oh noes!!1',
		writable: false,
		enumerable: false,
		configurable: false,
	}
);

Приведенный выше фрагмент определяет свойство с именем «0» (которое является индексом массива), но устанавливает свойство в значение, отличное от значения по умолчанию.

В таких крайних случаях механизм JavaScript использует все резервное хранилище элементов в качестве словаря, сопоставляя объект описания с каждым индексом массива.

23
Даже если только один элемент массива имеет объект описания не по умолчанию, резервное хранилище элементов для всего массива переходит в этот медленный и неэффективный режим. Избегайте использования Object.defineProperty для индексов элементов!

Эпилог

Это выступление дало нам понимание того, как работают движки JavaScript, как хранятся объекты и массивы, как оптимизируется доступ к свойствам с помощью Shapes и IC, а также как хранятся массивы. Основываясь на этих знаниях, мы определили несколько практических советов по кодированию, которые могут помочь повысить производительность:

  • Объекты всегда инициализируются одним и тем же образом, и в конечном итоге они получают одну и ту же форму.
  • Не изменяйте объекты описания элементов массива, их можно эффективно хранить.

отметка

  • Структура и код этой статьи взяты из презентации JavaScript Engines: The Good Parts™ Матиаса Байненса и Бенедикта Мёрера на JSConf EU 2018. Адрес видео: https://www.youtube.com/watch?v=5nmpokoRaZI&index=11&list=PL37ZVnwpeshG2YXJkun_lyNTtM-Qb3MKa
  • Также читайте блог этого выступления: https://mathiasbynens.be/notes/shapes-ics