Движок JavaScript: что внутри

JavaScript

Введение

Чтобы разобраться в том, как работает механизм обработки кода (иначе говоря, движок JavaScript), надо понять, что происходит при выполнении кода. Такие знания помогают программистам писать лучший, более быстрый и умный код.

Движки JavaScript — это не что иное, как программы, преобразующие код на JavaScript в код более низкого уровня, который компьютер сможет понять. Эти движки встроены в браузеры и веб-серверы (Node.js), что даёт возможность выполнять код и осуществлять компиляцию во время выполнения.

Разве JavaScript — это не интерпретируемый язык?

Краткий ответ: это зависит от реализации. Обычно JavaScript относят к интерпретируемым языкам, хотя вообще-то он компилируется. Современные компиляторы JavaScript фактически выполняют JIT-компиляцию, т.е. компиляцию «на лету», которая осуществляется во время работы программы.

Движок

Существует множество разных движков. У каждого из них внутри есть что-то вроде конвейера с интерпретатором и конвейера с оптимизатором и компилятором. Интерпретатор генерирует байт-код, а оптимизатор выдаёт оптимизированный машинный код. Далее в статье в качестве примера будет использоваться движок V8.

V8 — это высокопроизводительный движок от Google с открытым исходным кодом JavaScript и WebAssembly, написанный на языке C++.Он используется в Chrome, Node.js и других платформах и реализует ECMAScript и WebAssembly (см.v8.dev).

Что внутри движка

Всякий раз, когда JavaScript-код отправляется в движок V8, он проходит ряд этапов для отображения console.log:

Парсер

Движок выполняет то, что мы называем лексическим анализом. Это первое, что происходит с файлом JavaScript при попадании в движок. Код разбивается на части, называемые токенами, для выявления их назначения, после чего мы узнаём, что код пытается сделать.

Абстрактное синтаксическое дерево (AST)

На основе этих токенов создаётся то, что мы называем AST. Синтаксическое дерево — это древовидное представление синтаксической структуры кода JavaScript, и мы можем использовать этот инструмент для анализа преобразования кода AST.

Интерпретатор

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

Байт-код не является таким низкоуровневым, как машинный код, но всё же может быть интерпретирован движком JavaScript для выполнения кода.

Профайлер

Профайлер отвечает за проверку кода. Он вызывает специальное средство контроля, которое отслеживает код и наблюдает за ходом его выполнения, обращая наше внимание на то, как можно оптимизировать код. Выдаёт, например, информацию о том, сколько раз код запускался, какие типы используются и как мы можем его оптимизировать?

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

Оптимизирующий компилятор

Задача оптимизирующего компилятора — определить, что делает программа, подлежащая оптимизации, и создать из неё оптимизированную программу, выполняющую всё то же самое, только быстрее.

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

Деоптимизация

Оптимизирующий компилятор на основе имеющихся у него данных профилирования делает определённые предположения, а затем выдаёт высокооптимизированный машинный код. Но вполне возможно, что в какой-то момент одно из предположений окажется неверным. И тогда оптимизирующий компилятор может деоптимизировать код.

Объектная модель JavaScript

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

  • Enumerable → определяет, перечисляется ли свойство в циклах forin.
  • Value → само значение.
  • Writable → определяет, можно ли свойство изменить.
  • Configurable → определяет, можно ли удалить свойство.

Оптимизация доступа к свойству

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

Скрытые классы работают аналогично фиксированным макетам (классам) объектов, используемым в таких языках, как Java (за исключением того, что они создаются во время выполнения). Магия здесь в том, что несколько объектов могут иметь один и тот же скрытый класс, поэтому необходимы только минимальные ресурсы, а код становится быстрее:

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

Тема оптимизации очень обширна и может стать предметом обсуждения отдельной статьи. 

Встроенные кэши

Цель встроенного кэширования в том, чтобы ускорить привязку метода времени выполнения. Происходит это за счёт запоминания результатов поиска предыдущего метода непосредственно в месте вызова.Встроенное кэширование особенно полезно для динамически типизированных языков, где большинство, если не все привязки методов происходят во время выполнения и где виртуальные таблицы методов часто не могут быть использованы.

Основная причина существования скрытых классов — концепция встроенных кэшей. Движки JavaScript используют встроенные кэши для запоминания информации о том, где найти свойства в объектах. Поэтому нет необходимости повторять дорогостоящий поиск свойств при каждом доступе к ним. Зачем каждый раз искать свойства, когда со встроенными кэшами это значительно быстрее?

Выводы

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

Читайте также:


Перевод статьи Leonardo Freitas: Inside the JavaScript Engine