Как стать автором
Обновить
1057.28
OTUS
Цифровые навыки от ведущих экспертов

Основы JavaScript: почему вы должны знать, как работает JS-движок

Время на прочтение7 мин
Количество просмотров13K
Автор оригинала: freecodecamp.org

Для будущих учащихся на курсе "JavaScript Developer. Basic" подготовили перевод полезного материала.

Приглашаем также на открытый вебинар по теме
«Какими задачами проверяют ваше знание JavaScript»: возьмем тестовые вопросы из разных систем и посмотрим, о чем эти вопросы, что они проверяют и что нужно знать, чтобы на них правильно ответить.


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

Ниже вы увидите однострочную функцию, которая возвращает свойство lastName переданного аргумента. Просто добавив одно свойство к каждому объекту, мы получим падение производительности более чем на 700%!

Объясню подробнее почему так происходит. Отсутствие статической типизации в JavaScript приводит к такому поведению. Если рассматривать это как преимущество перед другими языками, такими как C# или Java, то в данном случае получается скорее "Faustian bargain" ("Фаустовская сделка". Жертвование духовных ценностей ради материальных выгод; происхождение выражения связано с именем Дж. Фауста).

Торможение на полной скорости

Обычно нам не нужно знать внутреннюю часть движка, который работает с нашим кодом. Производители браузеров вкладывают значительные средства в то, чтобы сделать так, чтобы движки запускали код очень быстро.

Здорово!

Пусть другие делают тяжелую работу. Зачем беспокоиться о том, как работают движки?

В нашем примере кода ниже, у нас есть пять объектов, в которых хранятся имена и фамилии персонажей из Star Wars (Звездных Войн). Функция getName возвращает значение фамилии. Измерим общее время выполнения этой функции:

(() => {   const han = {firstname: "Han", lastname: "Solo"};  const luke = {firstname: "Luke", lastname: "Skywalker"};  const leia = {firstname: "Leia", lastname: "Organa"};  const obi = {firstname: "Obi", lastname: "Wan"};  const yoda = {firstname: "", lastname: "Yoda"};  const people = [    han, luke, leia, obi,     yoda, luke, leia, obi   ];  const getName = (person) => person.lastname;

пример однострочного кода

console.time("engine");  for(var i = 0; i < 1000 * 1000 * 1000; i++) {     getName(people[i & 7]);   }  console.timeEnd("engine"); })();

На Intel i7 4510U время выполнения составляет около 1.2 секунд. Пока всё хорошо. Теперь мы добавим еще одно свойство к каждому объекту и выполним его снова.

(() => {  const han = {    firstname: "Han", lastname: "Solo",     spacecraft: "Falcon"};  const luke = {    firstname: "Luke", lastname: "Skywalker",     job: "Jedi"};  const leia = {    firstname: "Leia", lastname: "Organa",     gender: "female"};  const obi = {    firstname: "Obi", lastname: "Wan",     retired: true};  const yoda = {lastname: "Yoda"};
const people = [    han, luke, leia, obi,     yoda, luke, leia, obi];
const getName = (person) => person.lastname;
console.time("engine");  for(var i = 0; i < 1000 * 1000 * 1000; i++) {    getName(people[i & 7]);  }  console.timeEnd("engine");})();

Наше время исполнения теперь составляет 8.5 секунд, что примерно в 7 раз медленнее, чем в нашей первой версии. Это похоже на торможение на полной скорости. Как такое могло случиться?

Пора присмотреться к работе движка повнимательнее.

Объединенные Силы: Интерпретатор и Компилятор

Движок — это та часть (компонент) программы, которая читает и выполняет исходный код. У каждого крупного производителя браузера есть свой движок. Mozilla Firefox имеет Spidermonkey, Microsoft Edge — это Chakra/ChakraCore, а Apple Safari называет свой движок JavaScriptCore. Google Chrome использует V8, который также является движком для Node. js. Выпуск V8 в 2008 году ознаменовал поворотный момент в истории движков. V8 заменил браузеру относительно медленный интерпретатор JavaScript.

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

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

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

Основная идея современных движков — объединить лучшее из обоих миров:

  • Быстрый запуск интерпретатора.

  • Быстрое выполнение компилятора.

Современный движок использует интерпретатор и компилятор. Источник: imgflip
Современный движок использует интерпретатор и компилятор. Источник: imgflip

Осуществление обеих целей начинается с интерпретации. Параллельно движок помечает часто выполняемые части кода как "Hot Path" ("Горячий Путь") и передает их компилятору вместе с контекстной информацией, собранной во время выполнения. Этот процесс позволяет компилятору адаптировать и оптимизировать код под текущий контекст.

Поведение компилятора мы называем "Just in Time" или просто JIT (Just-in-time compilation, компиляция «на лету»).

При хорошей работе движка возможны некоторые сценарии, в которых JavaScript даже превосходит C++. Неудивительно, что большая часть его усилий идет на “contextual optimisation” ("контекстную оптимизацию").

Взаимодействие между Интерпретатором и Компилятором
Взаимодействие между Интерпретатором и Компилятором

Static Types (Статическая типизация) во время Runtime (Время выполнения): Inline Caching (Встроенное Кэширование)

Inline-кэширование, или IC, является основным методом оптимизации в движках JavaScript. Интерпретатор должен осуществить поиск, прежде чем он сможет получить доступ к свойствам объекта. Это свойство может быть частью прототипа объекта, оно должно иметь возможность доступа к нему с помощью метода Геттера (getter method) или даже через прокси-сервер. Поиск свойства достаточно затратный процесс с точки зрения скорости исполнения.

Движок присваивает каждому объекту “тип” ("type"), который он генерирует во время выполнения. V8 называет эти "типы" ("types"), которые не входят в стандарт ECMAScript, скрытые классы или формы объектов. Для того чтобы два объекта имели одну и ту же форму объекта, они должны обладать точно одинаковыми свойствами в одном и том же порядке. Таким образом, объект {firstname: "Han", lastname: "Solo"} будет присвоен к другому классу, нежели {lastname: "Solo", firstname: "Han"}.

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

Что делает Inline Caching, так это исключает операции поиска. Неудивительно, что это приводит к значительному повышению производительности.

Возвращаясь к нашему предыдущему примеру: Все объекты в первом запуске имели только два свойства, firstname и lastname, в одном и том же порядке. Допустим, внутреннее имя этой формы объекта — p1. Когда компилятор применяет IC, он предполагает, что функция только передает форму объекта p1 и сразу же возвращает значение lastname.

Inline Caching в действии (Мономорфное)
Inline Caching в действии (Мономорфное)

Однако во втором прогоне мы имели дело с 5 различными формами объектов. Каждый объект имел дополнительное свойство, и в yoda отсутствовало firstname. Что происходит, когда мы имеем дело с несколькими фигурами объектов?

Вмешательство Ducks или различная типизация (Intervening Ducks or Multiple Types)

Функциональное программирование использует хорошо известную концепцию “duck typing” ( "утиной типизации"), при котором хороший код (good code) вызывает функции, способные работать с несколькими типами. В нашем случае, пока переданный объект имеет свойство "lastname", все в порядке.

Внутреннее кэширование исключает затратный поиск ячейки памяти свойства. Это работает лучше всего, когда при каждом доступе к свойству объект имеет одинаковую форму. Это называется мономорфным IC.

Если у нас есть до четырех различных форм объекта, то мы находимся в полиморфном состоянии IC. Как и в мономорфном состоянии, оптимизированный машинный код "знает" уже все четыре местоположения. Но он должен проверить, к какой из четырех возможных форм объекта относится переданный аргумент. Это приводит к снижению производительности.

Как только мы выходим за порог четырех, это резко снижает производительность. Теперь мы находимся в так называемой мегаморфной IC. В этом состоянии больше не происходит локального кэширования ячеек памяти. Вместо этого, их нужно просматривать из глобальной кэш-памяти. Это приводит к экстремальному падению производительности, которое мы видели выше.

Полиморфизм и Мегаморфизм в действии

Ниже мы видим полиморфный Inline Cache с 2 различными формами объектов.

Полиморфный Inline Cache
Полиморфный Inline Cache

И мегаморфный IC из нашего примера кода с 5-ю разными формами объектов:

Мегаморфный Inline Cache
Мегаморфный Inline Cache

Класс JavaScript в помощь

Итак, у нас было 5 форм объектов и мы столкнулись с мегаморфной IC. Как мы можем это исправить?

Мы должны убедиться, что движок отмечает все 5 наших объектов и их формы как одинаковые. Это означает, что все создаваемые нами объекты должны будут наделяться всеми возможными свойствами. Мы могли бы использовать объектные литералы (object literals), но я нахожу JavaScript-классы лучшим решением.

Для свойств, которые не определены, мы просто передадим null или пропустим. Конструктор убеждается в том, чтобы эти поля инициализировались значением:

(() => {  class Person {    constructor({      firstname = '',      lastname = '',      spaceship = '',      job = '',      gender = '',      retired = false    } = {}) {      Object.assign(this, {        firstname,        lastname,        spaceship,        job,        gender,        retired      });    }  }
const han = new Person({    firstname: 'Han',    lastname: 'Solo',    spaceship: 'Falcon'  });  const luke = new Person({    firstname: 'Luke',    lastname: 'Skywalker',    job: 'Jedi'  });  const leia = new Person({    firstname: 'Leia',    lastname: 'Organa',    gender: 'female'  });  const obi = new Person({    firstname: 'Obi',    lastname: 'Wan',    retired: true  });  const yoda = new Person({ lastname: 'Yoda' });  const people = [    han,    luke,    leia,    obi,    yoda,    luke,    leia,    obi  ];  const getName = person => person.lastname;  console.time('engine');  for (var i = 0; i < 1000 * 1000 * 1000; i++) {    getName(people[i & 7]);  }  console.timeEnd('engine');})();

Когда мы снова выполняем эту функцию, то видим, что время ее выполнения возвращается к 1.2 секундам. Задача выполнена!

Резюме

Современные JavaScript-движки сочетают в себе преимущества интерпретатора и компилятора: Быстрый запуск приложений и быстрое выполнение кода.

Inline Caching — это мощный метод оптимизации. Лучше всего он работает, когда в оптимизированную функцию передается только одна форма объекта.

Мой пример показал эффективность Inline кэширования различных типов и проблемы при работе с мегаморфными кэшами.

Использование классов JavaScript является хорошей практикой. Статически типизированные транспайлеры (Static typed transpilers), такие как TypeScript, делают мономорфное IC более привлекательным для использования.


Узнать подробнее о курсе "JavaScript Developer. Basic"

Смотреть открытый вебинар по теме: «Какими задачами проверяют ваше знание JavaScript»

Теги:
Хабы:
Всего голосов 11: ↑8 и ↓3+5
Комментарии16

Публикации

Информация

Сайт
otus.ru
Дата регистрации
Дата основания
Численность
101–200 человек
Местоположение
Россия
Представитель
OTUS