Рубрики
Без рубрики

Как работает JavaScript: под капотом двигателя V8

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

Автор оригинала: FreeCodeCamp Community Member.

Сегодня мы рассмотрим под капотом двигателя V8 JavaScript и выясните, насколько именно выполнен JavaScript.

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

Задний план

Веб-стандарты являются набором правил, которые браузер реализуют. Они определяют и описывают аспекты World Wide Web Отказ

W3C это международное сообщество, которое развивает открытые стандарты для Интернета. Они убедитесь, что все следует одни и те же руководящим принципам и не должны поддерживать десятки совершенно разных сред.

Современный браузер – это довольно сложный участок программного обеспечения с кодовой базой десятки миллионов строк кода Отказ Так что это разделено в много модулей, ответственных за разную логику.

И две из наиболее важных частей браузера являются двигатель JavaScript и двигатель рендеринга.

Мигать Это двигатель рендеринга, который отвечает за весь конвейер рендеринга, включая DOM деревьев, стилей, события и интеграцию V8. Он разбирает дерево DOM, решает стили и определяет визуальную геометрию всех элементов.

Хотя постоянно контролируя динамические изменения с помощью кадров анимации, мгновение окрашивает содержание на вашем экране. ДВИГАТЕЛЬ JS – большая часть браузера – но мы еще не попали в эти детали.

JavaScript Engine 101.

Двигатель JavaScript выполняет и компилирует JavaScript в собственный код машины. Каждый основной браузер разработал свой собственный двигатель JS: Chrome Google использует V8, Safari использует JavaScriptCore, и Firefox использует Spidermonkey.

Мы будем особенно работать с V8 из-за его использования в Node.js и Electron, но другие двигатели построены так же.

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

Мы будем работать с Зеркало V8 на Github Поскольку он предоставляет удобный и известной интернет-пользовательской интернет-пользователем для перемещения кодовой базы.

Подготовка исходного кода

Первое, что V8 необходимо сделать, это скачать исходный код. Это может быть сделано через сеть, кэш или обслуживание работников.

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

Сканер принимает файл JS и преобразует его в список известных токенов. Есть список всех токенов JS в ключевых слов. файл atxt Отказ

Парсер поднимает и создает Абстрактное синтаксическое дерево (AST) : Представление дерева исходного кода. Каждый узел дерева обозначает конструю, возникающую в коде.

Давайте посмотрим на простой пример:

function foo() {
  let bar = 1;
  return bar;
}

Этот код будет производить следующую структуру дерева:

Вы можете выполнить этот код, выполнив превышение предварительного заказа (root, влево, справа):

  1. Определите Foo функция.
  2. Объявить бар Переменная.
  3. Назначить 1 к бар Отказ
  4. Возвращение бар из функции.

Вы также посмотрите Варьируемоеproxy – элемент, который соединяет абстрактную переменную к месту в памяти. Процесс разрешения Варьируемоеproxy называется Сфера анализа Отказ

В нашем примере результат процесса будет все Варьируемоеproxy указывая на то же самое бар Переменная.

Парадигма справедливого (JIT)

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

Наиболее распространенным способом преобразования кода является выполнение предложений подбора времени. Он работает точно так, как это звучит: код преобразован в машинный код до выполнения вашей программы во время стадии компиляции.

Этот подход используется многими языками программирования, такими как C ++, Java и другие.

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

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

Чтобы преобразовать код быстрее и более эффективно для динамических языков, был создан новый подход, который был создан с именем Rest-Time (JIT) Compilation. Он сочетает в себе лучшее от интерпретации и компиляции.

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

Однако есть вероятность того, что тип может измениться. Нам нужно сделать оптимизм Compabled Code и Foleback к интерпретации вместо этого (после этого мы можем перекомпилировать функцию после получения нового типа обратной связи).

Давайте рассмотрим каждую часть Compilation JIT более подробно.

Устный переводчик

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

Инструкции по байтому коду также имеют метаданные, такие как позиции источника для будущей отладки. Как правило, инструкции по байтому коду соответствуют абстракциям JS.

Теперь давайте возьмем наш пример и генерируем байтовый код для него вручную:

LdaSmi #1 // write 1 to accumulator
Star r0   // read to r0 (bar) from accumulator 
Ldar r0   // write from r0 (bar) to accumulator
Return    // returns accumulator

Зажигание имеет что-то называемое аккумулятором – место, где вы можете хранить/чтение значения.

Аккумулятор позволяет избежать необходимости нажатия и выскакивания верхней части стека. Это также неявный аргумент для многих байтовых кодов и обычно содержит результат операции. Вернуться неявно возвращает аккумулятор.

Вы можете проверить весь доступный байтовый код В соответствующем исходном коде Отказ Если вы заинтересованы в том, как другие концепции JS (такие как петли и async/a aquait) представлены в байтовом коде, я считаю полезным читать через эти Тестовые ожидания Отказ

Исполнение

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

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

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

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

Чтобы решить эту проблему, V8 отделяет структуру объекта из самого значений с Формы объекта (или карты внутренне) и вектор значений в памяти.

Например, мы создаем объектный литерал:

let c = { x: 3 }
let d = { x: 5 }
c.y = 4

В первой строке он будет производить форму Карта [C] что имеет свойство х со смещением 0.

Во второй строке V8 повторно использует ту же форму для новой переменной.

После третьей строки это создаст новую форму Карта [C1] для недвижимости y С компенсацией 1 и создать ссылку на предыдущую форму Карта [C] Отказ

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

Формы объекта являются по существу связанными списками. Так что если вы пишете C.x V8 пойдет в голову списка, найти y там двигаться в связанную форму, и, наконец, он получает х и читает смещение от этого. Тогда он пойдет в вектор памяти и вернет первый элемент из него.

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

Чтобы решить эту проблему в V8, вы можете использовать Встроенный кэш (IC) Отказ Он запоминает информацию о том, где найти свойства на объектах, чтобы уменьшить количество поисков.

Вы можете подумать об этом как сайтом прослушивания в вашем коде: он отслеживает все Позвоните , Магазин и Нагрузка События внутри функции и записывает все формы, проходящие мимо.

Структура данных для хранения IC называется Обратная связь вектор Отказ Это просто массив, чтобы сохранить все ICS для функции.

function load(a) {
  return a.key;
}

Для функции выше, вектор обратной связи будет выглядеть так:

[{ slot: 0, icType: LOAD, value: UNINIT }]

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

Давайте назовем эту функцию с разными аргументами и посмотрите, как изменится встроенный кеш.

let first = { key: 'first' } // shape A
let fast = { key: 'fast' }   // the same shape A
let slow = { foo: 'slow' }   // new shape B

load(first)
load(fast)
load(slow)

После первого звонка нагрузка Функция, наш встроенный кэш получает обновленное значение:

[{ slot: 0, icType: LOAD, value: MONO(A) }]

Это значение теперь становится мономорфным, что означает, что этот кеш может разрешить только для формирования A.

После второго вызова V8 проверит значение IC, и он увидит, что это мономорфно и имеет ту же форму, что и быстро Переменная. Так что это быстро возвращается и разрешает его.

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

[{ slot: 0, icType: LOAD, value: POLY[A,B] }]

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

Для более быстрого кода вы может Инициализируйте объекты с тем же типом и не изменять их структуру слишком много.

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

Встроенные кэши также отслеживают, как часто они призваны решать, является ли это хорошим кандидатом для оптимизации компилятора – Turbofan.

Компилятор

Зажигание только заставляет нас. Если функция достаточно горячая, она будет оптимизирована в компиляторе, Турбофан , чтобы сделать его быстрее.

Turbofan принимает код байта из обратной связи зажигания и типа (вектор обратной связи) для функции, применяет набор снижений на основе него, и производит машинный код.

Как мы видели раньше, введите обратную связь не гарантируют, что он не изменится в будущем.

Например, Turbofan оптимизировал код, основанный на предположении, что какое-то добавление всегда добавляет целые числа.

Но что бы произошло, если это получило строку? Этот процесс называется деоптимизация. Выбрасываем оптимизированный код, вернемся к интерпретающему коду, резюме выполнения и обновления типа обратной связи.

Резюме

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

Чтобы обобщить, давайте посмотрим на конвейер сборник с вершины.

Мы пойдем через это шаг за шагом:

  1. Все начинается с получения кода JavaScript из сети.
  2. V8 разбирается исходный код и превращает его в абстрактное синтаксическое дерево (AST).
  3. На основании этого AST переводчик зажигания может начать делать свою вещь и производить Bytecode.
  4. В этот момент двигатель начинает выполнять код и собирать обратную связь типа.
  5. Чтобы он запустился быстрее, байтовый код может быть отправлен на оптимизирующий компилятор вместе с данными обратной связи. Оптимизирующий компилятор делает определенные допущения на основе него, а затем производит высокооптимированный машинный код.
  6. Если в какой-то момент один из предположений оказывается неправильным, оптимизирующим компилятором de-оптимизирует и возвращается к интерпретатору.

Это оно! Если у вас есть какие-либо вопросы о конкретном этапе или хотите узнать больше подробнее об этом, вы можете погрузиться в исходный код или ударить меня на Twitter Отказ

дальнейшее чтение