Обзор процесса выполнения JavaScript Engine V8

JavaScript

Эта статья была впервые опубликована в публичном аккаунте vivo Internet Technology WeChat.
Связь:Билеты.WeChat .QQ.com/есть/он __jq это 1-е...
Автор: Лай Юнгао

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

A, V8 источники

V8 принимает свое имя из автомобиля «V-8-цилиндровый» (двигатель V8). Двигатель V8 был в основном разработан в Соединенных Штатах и ​​широко известен своей высокой мощностью. Название двигателя V8 состоит в том, что Google показывает пользователей, что это мощный и быстрый JavaScript Engine.

V8 разработан на основе Chrome, и позже он не ограничивается ядром браузера. До сих пор V8 использовался во многих сценариях, таких как популярные nodejs, weex, быстрое приложение и ранний RN.

3. Ранняя архитектура V8

Двигатели рождения V8 приходят с миссией, это революционно с точки зрения скорости и вспоминания о памяти. Архитектура JavaScriptscore генерируется путем Bytecode, который выполняется Bytecode. Google JavaScriptCore считает, что эта архитектура нет, генерируйте код байта, будет пустой тратой времени, лучше напрямую генерировать быстрый машинный код. V8 Так рано в архитектурном дизайне очень агрессивен, используя компиляцию непосредственно в машинный код. Позже Google позже доказала, что эта архитектура состоит в том, чтобы улучшить скорость, но она также вызвала проблемы потребления памяти. Мы можем посмотреть на начальную блок-схему V8:

Ранний V8 имел компиляторы Full-Codegen и Crankshaft. V8 сначала использует Full-Codegen для единовременной компиляции всех кодов для генерации соответствующего машинного кода. Во время выполнения JS встроенный в V8 Profiler отфильтровывает горячие функции и записывает типы обратной связи параметров, а затем передает их Crankshaft для оптимизации. Таким образом, Full-Codegen генерирует неоптимизированный машинный код, а Crankshaft генерирует оптимизированный машинный код.

Четыре, архитектура V8 раннего дефекта

С усложненной версией введения веб-страницы V8 также постепенно выявила недостатки собственной архитектуры:

  1. Компиляция Full-Codegen напрямую генерирует машинный код, что приводит к большому объему памяти.

  2. Компиляция Full-Codegen напрямую генерирует машинный код, что приводит к длительному времени компиляции и низкой скорости запуска.

  3. Crankshaft не может оптимизировать блоки кода, разделенные ключевыми словами, такими как try, catch и наконец.

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

5. Существующая архитектура V8

Чтобы устранить вышеуказанные недостатки, V8 использует структуру JavaScriptCore для генерации байт-кода. Вам кажется, что Google снова вернулся? V8 использует метод генерации байт-кода, и общий процесс выглядит следующим образом:

Ignition — это интерпретатор V8, первоначальная цель которого заключалась в том, чтобы уменьшить потребление памяти на мобильных устройствах. До Ignition код, сгенерированный базовым компилятором V8 Full-codegen, обычно составлял почти треть всей кучи JavaScript Chrome. Это оставляет меньше места для фактических данных веб-приложения.

Байт-код Ignition может напрямую генерировать оптимизированный машинный код с помощью TurboFan без необходимости повторной компиляции из исходного кода, как это делает Crankshaft. Байт-код Ignition обеспечивает более чистую и менее подверженную ошибкам базовую модель выполнения в V8, упрощая механизмы деоптимизации, ключевую особенность адаптивной оптимизации V8. Наконец, поскольку генерация байт-кода выполняется быстрее, чем генерация базового скомпилированного кода Full-codegen, активация Ignition часто улучшает время запуска скрипта и, следовательно, загрузку веб-страницы.

TurboFan — это оптимизирующий компилятор для V8, и проект TurboFan изначально был запущен в конце 2013 года для устранения недостатков Crankshaft. Crankshaft может оптимизировать только часть языка JavaScript. Например, он не предназначен для оптимизации кода JavaScript со структурированной обработкой исключений, то есть блоков кода, разделенных ключевыми словами try, catch и finally. Добавить поддержку новых языковых функций в Crankshaft сложно, потому что эти функции почти всегда требуют написания кода, специфичного для архитектуры, для девяти поддерживаемых платформ.

Преимущества перехода на новую архитектуру

Отличие V8 от разных архитектур памяти, как:

В заключение:Хорошо видно, что память архитектуры Ignition+TurboFan составляет более половины памяти архитектуры Full-codegen+Crankshaft.

Сравнение повышения скорости различных архитектур показано на рисунке:

В заключение:Хорошо видно, что архитектура Ignition+TurboFan на 70% быстрее, чем архитектура Full-codegen+Crankshaft.

Далее мы кратко объясним каждый процесс существующей архитектуры:

Six, лексический анализ V8 и синтаксический анализ

Учебные студенты Compiler Comporer могут знать, файл JS - это просто источник, машина не может быть выполнена, лексический анализ - это разделить источник строки из серии генерации токена, на рисунке ниже показаны различные типы, соответствующие другому токену строки.

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

Семь, абстрактное синтаксическое дерево V8 AST

На следующем рисунке показана структура данных абстрактного синтаксического дерева функции добавления.

После этапа синтаксического анализа V8 следующим шагом является создание байт-кода на основе абстрактного синтаксического дерева. Как показано на рисунке ниже, функция добавления генерирует соответствующий байт-код:

Функция класса BytecodeGenerator заключается в генерации соответствующего байт-кода в соответствии с абстрактным синтаксическим деревом.Разные узлы будут соответствовать функции генерации байт-кода, и функция начинается с Visit****. Генерируется байт-код функции, соответствующий знаку + на следующем рисунке:

void BytecodeGenerator::VisitArithmeticExpression(BinaryOperation* expr) {
  FeedbackSlot slot = feedback_spec()->AddBinaryOpICSlot();
  Expression* subexpr;
  Smi* literal;
  
  if (expr->IsSmiLiteralOperation(&subexpr, &literal)) {
    VisitForAccumulatorValue(subexpr);
    builder()->SetExpressionPosition(expr);
    builder()->BinaryOperationSmiLiteral(expr->op(), literal,
                                         feedback_index(slot));
  } else {
    Register lhs = VisitForRegisterValue(expr->left());
    VisitForAccumulatorValue(expr->right());
    builder()->SetExpressionPosition(expr);  //  保存源码位置 用于调试
    builder()->BinaryOperation(expr->op(), lhs, feedback_index(slot)); //  生成Add字节码
  }
}

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

Сгенерировать байт-код, как выполняется байт-код? Далее объясните:


8. Байт-код

Во-первых, давайте поговорим о Bytecode V8:

  1. Каждый байтовый код указывает свой ввод и вывод как операнда регистра

  2. Зажигание использует регистры r0, r1, r2... и накопительный регистр.

  3. Регистры регистров: параметры функций и локальные переменные, хранящиеся в видимых пользователю регистрах.

  4. Аккумулятор: это невидимый пользователю регистр, используемый для хранения промежуточных результатов.

ДОБАВЬТЕ байт-код, как показано ниже:

выполнение байт-кода

Следующая серия диаграмм показывает изменения в соответствующих регистрах и аккумуляторах при выполнении каждого байт-кода.Функция добавления передает параметры 10 и 20, и окончательный результат, возвращаемый аккумулятором, равен 50.

Каждый байт-код соответствует функции обработки, а адрес, сохраненный обработчиком байт-кода, находится в dispatch_table_. Когда байт-код выполняется, для выполнения вызывается соответствующий обработчик байт-кода. Член класса Interpreter dispatch_table_ содержит адрес обработчика для каждого байт-кода.

Например, функция обработки, соответствующая байт-коду ADD, имеет вид (при выполнении байт-кода ADD будет вызван класс InterpreterBinaryOpAssembler):

IGNITION_HANDLER(Add, InterpreterBinaryOpAssembler) {
   BinaryOpWithFeedback(&BinaryOpAssembler::Generate_AddWithFeedback);
}
  
void BinaryOpWithFeedback(BinaryOpGenerator generator) {
    Node* reg_index = BytecodeOperandReg(0);
    Node* lhs = LoadRegister(reg_index);
    Node* rhs = GetAccumulator();
    Node* context = GetContext();
    Node* slot_index = BytecodeOperandIdx(1);
    Node* feedback_vector = LoadFeedbackVector();
    BinaryOpAssembler binop_asm(state());
    Node* result = (binop_asm.*generator)(context, lhs, rhs, slot_index,                            
feedback_vector, false);
    SetAccumulator(result);  // 将ADD计算的结果设置到累加器中
    Dispatch(); // 处理下一条字节码
  
}

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

function add(x, y) {
  return x+y;
}
add(1, 2);
%OptimizeFunctionOnNextCall(add);
add(1, 2);

Оптимизированная функция V8 может быть вызвана, чтобы указать, какая функция напрямую, реализация инициативы по вызову Turbofan% OptimizeFunctionOnNextCall добавить оптимизацию функции, оптимизацию добавить функцию обратной связи в соответствии с параметрами последнего вызова, ясно, что эта обратная связь является целочисленной, поэтому аргумент основан на турбовентиляторной целочисленной оптимизации, непосредственно генерирующей машинный код, следующая функция вызывает непосредственно оптимизацию вызова хорошего машинного кода. (Примечание. Для выполнения V8 необходимо добавить --allow-natives-syntax, OptimizeFunctionOnNextCall в качестве встроенных функций, добавить только --allow-natives-syntax, JS может вызывать встроенные функции, иначе произойдет ошибка).

Соответствующий машинный код, сгенерированный функцией добавления JS, выглядит следующим образом:

Здесь понятие Small Interger Small Integer можно посмотреть в этой статье https://zhuanlan.zhihu.com/p/82854566

Если изменить входящий параметр функции добавления на символ

function add(x, y) {
  return x+y;
}
add(1, 2);
%OptimizeFunctionOnNextCall(add);
add(1, 2);

Оптимизированная функция добавления генерирует соответствующий машинный код следующим образом:

Сравнивая два приведенных выше рисунка, функция добавления передает разные параметры и генерирует разные машинные коды после оптимизации.

Если передается целое число, то это, по сути, прямой вызов ассемблерной инструкции добавления.

Если передается строка, она, по сути, вызывает встроенную функцию добавления V8.

На этом весь процесс выполнения V8 заканчивается. Там может не понять правильное место, пожалуйста, укажите статью.

  • Справочная статья

  1. v8.dev/docs

  2. docs.Google.com/present Вопрос А…

  3. docs.Google.com/present Вопрос А…

  4. zhuanlan.zhihu.com/p/82854566

Больше контента, пожалуйста, обратите вниманиеживые интернет-технологииПубличный аккаунт WeChat

Примечание. Чтобы перепечатать статью, сначала свяжитесь с учетной записью WeChat:labs2020соединять.