Сейчас большинство веб-приложений написаны на JavaScript - динамически типизируемом языке для скриптов с похожим на язык C синтаксисом
Код на JavaScript могут исполнять множество движков (виртуальных машин)
Хотя в JavaScript есть примитивные типы, многие операции заставляют их вести себя как объекты через автоматическую упаковку в объекты. У объекта в JavaScript есть свойства, у свойства - имя и значения, а сами свойства могут добавляться и удаляться во время исполнения
В JavaScript вместо наследование использованы прототипы: каждый объект хранит в себе ссылку на прототип другого объекта, тем самым наследуя его свойства. Объекты могут представлять собой цепочку прототипов, которую можно изменять во время исполнения
Конвертация типов в JavaScript позволяют выполнять такие операции:
var x = 1 + 2; // 3
x = 1 + "2"; // "12"
x = 1 + {}; // "1[object Object]"
x = +"23"; // 23
Из-за прототипов, свойств и динамической типизации JavaScript очень динамический, что представляет трудность для реализации виртуальной машины. Из-за этого появляются проблемы в разработке: приходящий тип объекта не соответствует ожидаемому
Далее появился TypeScript, позволяющий аннотировать типы и проверять их перед исполнением программы
Движок JavaScript работает так:
Спецификация JavaScript не указывает формат промежуточного кода, поэтому разработчик движка волен сам выбрать его формат. Интерпретатор также содержит сборщик мусора, который управляет памятью, и совершает оптимизации над кодом
Внутри компилятора:
После этого интерпретатор исполняет байткод
Из-за того, что JavaScript - динамически типизированный, выражение x + y может представлять из себя:
Из-за этого JavaScript обладает низкой производительностью
Главная цель для JavaScript - мгновенно исполнять код, чтобы веб-страницы загружались быстро. Поэтому парсер и интерпретатор не должны тратить ресурсы на оптимизации. Другой путь - компиляция после парсера, что замедляет время запуска
Сейчас в браузерах байткод после парсера сразу исполняется интерпретатором, а в другом потоке горячие регионы - часто вызывающиеся участки кода - компилируются и исполняются нативно
Всего можно выделить 4 уровня:
Если кусок кода вызывается больше какого-то выбранного числа, то далее выбирается нужные оптимизации исходя из информации о выполнении кода
Большая часть веб-приложений и мобильных приложений используют такую гибридную модель: часть кода исполняется в интерпретаторе, часть кода нативно как машинные инструкции
Старая версия движка JavaScriptCore использовала три уровня исполнения
В новой версии добавился новый уровень:
Позднее поняли, что с таким конвейером тяжело работать, поэтому упростили инфраструктуру
Интерпретатор имеет ряд достоинств
И недостатков: большое потребление памяти и низкая скорость по сравнению с машинным кодом
Интерпретацию можно ускорить с помощью AOT- и JIT-компиляций, скрытых классов (структур, с помощью которых можно быстрее обрабатывать сложные объекты). Также можно применить такие оптимизации:
Для определения операции из инструкции байткода не следует использовать блоки if/switch. Вместо этого можно использовать:
Прямое перенаправление (Direct Threading) - изменение указателя на текущую инструкцию на значение, равное коду инструкции (opcode)
Непрямое перенаправление (Indirect Threading) - переход делается не напрямую по коду инструкции, а через уровень косвенной адресации (сначала берётся адрес в таблице, затем по нему происходит переход)
Перенаправление токенов (Token Threading) - использование токенов-указателей
Встроенное кеширование (Inline Caching)
Вместо того, чтобы заново проверять типы для исполнения операции, можно для часто применяющихся ситуаций (например, для сложения двух чисел) перенаправлять на другой поток инструкций
Встроенное кеширование имеет несколько состояний