Успешно завершилась конференция JSConf EU 2018. Матиас Байненс, разработчики Google V8, и Бенедикт Мёрер выступили с докладом «JavaScript Engines: The Good Parts™». Эта статья познакомит вас с ключевыми моментами, упомянутыми в выступлении.
Лекция, часть 1: Движок JavaScript
JavaScript-движок
Механизм JavaScript анализирует исходный код и преобразует его в абстрактное синтаксическое дерево (AST). На основе AST интерпретатор создает байт-код. В этот момент движок выполняет код JavaScript. Чтобы ускорить процесс, байт-код отправляется компилятору вместе с данными профилирования. Компилятор делает определенные предположения на основе доступных данных профилирования, а затем генерирует оптимизированный машинный код.
Интерпретатор/компилятор в движке JavaScript
Объясните, как движки JavaScript выполняют ваш код, сравнив некоторые различия в реализации основных движков JavaScript.
Интерпретатор быстро создает неоптимизированный байт-код, компилятор занимает больше времени, но в итоге создает высокооптимизированный машинный код.
Интерпретаторы могут быстро генерировать байт-код, но выполнение байт-кода обычно не очень эффективно. Компиляторы, с другой стороны, занимают больше времени, но в итоге создают более эффективный машинный код. Существует компромисс между быстрым выполнением кода (интерпретатор) и увеличением времени выполнения кода с оптимальной производительностью (компилятор).
Лекция, часть 2: Объектная модель JavaScript
Спецификация ECMAScript в основном определяет все объекты как словари и сопоставляет строковые ключи с объектами описания.
JavaScript определяет массив как объект. Например, все ключи, включая индексы массива, явно представлены в виде строк. Первый элемент массива хранится по ключу "0".
Лекция, часть 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, который использует структуру данных словаря, которую мы обсуждали ранее: он содержит ключи в виде строк, и они указывают на объекты описания для своих соответствующих свойств.
Если каждый объект JS хранит объект описания, это приведет к большому дублированию и ненужным затратам памяти. Механизм JavaScript хранит формы этих объектов отдельно.
Все движки JavaScript используют Shape в качестве оптимизации, но не все называют его Shape:
- Академические документы называют это скрытыми классами.
- V8 называет это Картами
- Чакра называет это типами
- JavaScriptCore называет это структурами
- SpiderMonkey называет это Shapes Форма используется в речи единообразно.
Цепочки переходов и деревья переходов
Если объект указывает на фигуру и вы добавляете к нему новое свойство, как движок JavaScript найдет новую фигуру. Формы этого типа формируют так называемую «цепочку переходов» в движке JavaScript. Ниже приведен пример:
Нам даже не нужно хранить полную таблицу атрибутов для каждой фигуры. Вместо этого каждая форма должна знать только о новых свойствах, которые она вводит. Например, в этом случае нам не нужно хранить информацию о «x» в последней фигуре, потому что ее можно найти раньше в цепочке. Для этого каждая фигура связана с предыдущей фигурой:
Но что, если нет возможности создать цепочку переходов? Например, что, если у вас есть два пустых объекта и к каждому добавлены разные свойства?
const object1 = {};
object1.x = 5;
const object2 = {};
object2.y = 6;
В этом случае мы должны заменить цепочку ветвью, и в итоге мы получим дерево переходов:
Движок применяет некоторые оптимизации к объектам, которые уже содержат свойства. Либо начните с пустого объекта и добавьте «x», либо создайте объект, который уже содержит «x»:
const object1 = {};
object1.x = 5;
const object2 = { x: 6 };
Внутреннее поведение (ICS)
ICs являются ключевыми факторами JavaScript для быстрой работы! Механизм JavaScript для поиска информации с использованием интегральных схем, чтобы запомнить, где находятся объекты, чтобы уменьшить количество поисков. Вот функция getX, она загружается из объекта и атрибута "x":
function getX(o) {
return o.x;
}
Если мы запустим эту функцию в АО, она сгенерирует следующий байт-код:
Кроме того, в инструкцию get_by_id встраивается встроенный кэш, который состоит из двух неинициализированных слотов.
Лекция, часть 4: Эффективные массивы хранения
Массивы используют индексы массива для хранения свойств. Значения этих свойств называются элементами массива. Неразумно хранить объекты описания для каждого элемента массива. Индексированные свойства массива по умолчанию доступны для записи, перечисления и настройки, а механизм JavaScript хранит элементы массива отдельно от других свойств.
Взгляните на этот массив:
const array = [
'#jsconfeu',
];
Движок хранит массив длины 1 и указывает на фигуру, содержащую длину, со смещением 0.
Что если я изменю объект описания элемента массива?
// Please don’t ever do this!
const array = Object.defineProperty(
[],
'0',
{
value: 'Oh noes!!1',
writable: false,
enumerable: false,
configurable: false,
}
);
Приведенный выше фрагмент определяет свойство с именем «0» (которое является индексом массива), но устанавливает свойство в значение, отличное от значения по умолчанию.
В таких крайних случаях механизм JavaScript использует все резервное хранилище элементов в качестве словаря, сопоставляя объект описания с каждым индексом массива.
Эпилог
Это выступление дало нам понимание того, как работают движки 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