Наука о JavaScript движке: Как компьютеры читают ваш код

Источник: «The JavaScript Engine: How Machines Read Your Code»
Изучите работу движка JavaScript, от разбора кода до оптимизации его производительности, а также такие ключевые техники, как скрытые классы и управление памятью.

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

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

Что же именно происходит с кодом во время этого процесса?

Хотя существует несколько движков, я остановлюсь на самом популярном из них: V8, на котором работают Google Chrome и Node.js.

Если вы не только любите программировать, но и цените научную базу, пристегните ремни. Когда речь идёт о движке V8, путешествие обещает быть захватывающим (это был хороший каламбур?).

Краткая история JavaScript движков

На заре развития Интернета веб-страницы были преимущественно статичными. Они представляли собой смесь HTML и CSS, с небольшим количеством JavaScript для базовой интерактивности. В то время JavaScript был чисто интерпретируемым. Это означает, что код читался и выполнялся построчно, без какой-либо предварительной оптимизации. Это было сродни чтению книги вслух без предварительного беглого просмотра. Простота веб-страниц не требовала сверхбыстрого выполнения JavaScript.

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

Наступила эра современных JavaScript движков. Потребность в скорости породила такие движки, как:

Вот список всех движков JavaScript: https://en.wikipedia.org/wiki/List_of_ECMAScript_engines

Эти движки изменили способ выполнения JavaScript. Они ввели такие новшества, как компиляция JIT, которая проходила между интерпретацией и полной компиляцией, стремясь получить лучшее из обоих миров. Мы перешли от чтения книги вслух к тому, чтобы сначала пролистать её, выделить важные части, а затем увлечённо рассказывать.

Анатомия JavaScript движка

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

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

Давайте разберёмся в каждом шаге, для начала взглянув на диаграмму ниже:

Переход от JavaScript файла к машиночитаемому коду. Этапы: Parser, AST, Interpreter, Profiler и Compiler.
Переход от JavaScript файла к машиночитаемому коду. Этапы: Parser, AST, Interpreter, Profiler и Compiler.

Parser/Парсер: Декодирование языка

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

Абстрактное Дерево Синтаксиса (AST): Создание структуры

Эти токены служат строительными блоками для Абстрактного Дерева Синтаксиса (AST). AST — это аналог диаграммы, представляющей синтаксическую структуру вашего кода. Она отражает отношения, иерархии и поток, выступая в качестве моста между языком высокого уровня и следующими этапами выполнения.

Существует довольно интересный сайт AST Explorer, на котором можно посмотреть, как код парсится в AST.

Interpreter/Интерпретатор: Первый черновик выполнения

AST прокладывает путь для интерпретатора (он же Ignition в V8). Как следует из названия, интерпретатор считывает и выполняет AST строка за строкой, создавая базовый "байткод". Этот байткод, хотя и быстрее, чем прямая интерпретация JavaScript, не использует в полной мере потенциальную скорость выполнения.

Интерпретированный код vs. Компилированный код

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

Смотрите диаграмму ниже. В то время как интерпретированный JavaScript выводит "Байткод", скомпилированный JavaScript выводит "Машинный код".

Сравнение между языком высокого уровня (например, кодом JavaScript), интерпретируемым Байткодом и компилируемым Машинным Кодом
Сравнение между языком высокого уровня (например, кодом JavaScript), интерпретируемым Байткодом и компилируемым Машинным Кодом

Как тогда JavaScript, языку, известному своей отзывчивостью в Интернете, удаётся найти баланс?

JIT: Лучшее из двух миров

Современные движки JavaScript сияют благодаря Just-In-Time (JIT) Компиляции. JIT — это не статичный процесс, а динамичный и адаптивный. Он позволяет в первую очередь интерпретировать JavaScript для моментального запуска, а затем, по мере выполнения кода, компилировать его для оптимизации производительности.

Здесь на помощь приходит Profiler.

Profiler/Профайлер: Наблюдатель

В системе JIT профилировщик играет важную роль. Действуя как система наблюдения, он постоянно следит за выполняющимся кодом, чтобы выявить закономерности, особенно участки (hotspots/горячие точки), которые часто выполняются или требуют больших вычислительных затрат.

Compiler/Компилятор: Средство для повышения эффективности

Определив "горячие точки", компилятор (он же Turbofan в V8) берётся за дело и повышает производительность. Он берет рудиментарный байткод, созданный интерпретатором, и перерабатывает его в высокоэффективный машинный код, специально для этих "горячих точек". Это гарантирует, что ваше приложение не только быстро запустится, но и будет работать с максимальной эффективностью во время выполнения самых сложных задач.

Но что это за оптимизация? Давайте узнаем о ней.

Скрытые классы и Оптимизации

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

Что такое Скрытые Классы

Ускоренный доступ к свойствам и роль Скрытых Классов

  1. Эволюция объекта: Когда вы создаёте объект и начинаете добавлять свойства, движок JavaScript присваивает объекту скрытый класс. По мере добавления или изменения свойств движок переводит объект из одного скрытого класса в другой, отслеживая его эволюцию.
  2. Поиск свойств: При обращении к свойству вместо поиска по свойствам объекта движок может использовать скрытый класс в качестве "дорожной карты", чтобы быстро найти местоположение свойства.

Пример:

let obj = {};
// Скрытый Класс А присвоен.
obj.x = 10;
// Переход из Скрытого класса A -> B
obj.y = 20;
// Переход из Скрытого класса B -> C

Если движок видит, что другой объект повторяет ту же схему (сначала добавляется x, затем y), он может разумно предсказать его форму и быстрее получить доступ к свойствам.

Встроенное кэширование (IC)

Что такое Встроенное Кэширование

Совместная работа Скрытых Классов и Встроенного Кэширования

Пример:

function getColor(car) {
return car.color;
}
const myCar = { color: 'red' };
getColor(myCar); // Первый доступ: нормальный поиск
getColor(myCar); // Последующий доступ: быстрый поиск через IC

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

Улучшение производительности в реальных условиях

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

Сборка мусора и Управление памятью

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

Сборщик мусора использует несколько алгоритмов и стратегий, определяющих, какая память может быть безопасно освобождена. Одна из основных стратегий, используемых во многих современных JavaScript-движках, таких, как V8 (используется в Google Chrome и Node.js), называется "mark-and-sweep".

Алгоритм "mark-and-sweep" состоит из двух фаз:

  1. Mark/Отметка: сборщик мусора просматривает память, начиная с "корней" (глобальные переменные, переменные текущей выполняющейся функции и т. д.), и отмечает все достижимые объекты. Достижимые объекты — это те, которые доступны прямо или косвенно из корней. Каждый объект, к которому можно получить доступ, помечается как "используемый".
  2. Sweep/Очистка: После того как все достижимые объекты помечены, сборщик мусора прочёсывает память, выявляя не помеченные объекты. Эти не помеченные объекты считаются недостижимыми и, следовательно, не нужны программе. Память, занятая этими неотмеченными объектами, освобождается.
Анимация стратегии Сборки мусора \"mark-and-sweep\"
Анимация стратегии Сборки мусора "mark-and-sweep"

Алгоритм "mark-and-sweep" помогает избежать преждевременного освобождения памяти, которое может произойти, если будет собран объект, который всё ещё нужен программе.

Если вам интересно, у меня есть ещё одна статья о сборке мусора и утечке памяти:

Лучшие практики для JavaScript-разработчиков

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

Написание дружественного к движку JavaScript

Распространённые ловушки и как их избежать

Заключение

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

Если у вас есть вопросы, пишите в комментариях, и буду рад помочь.

Дополнительные материалы

Предыдущая Статья

Подробно: Знакомство с Random

Следующая Статья

Совет по безопасности: Не используйте nl2br()!