"Простое объяснение JavaScript"ряд:
- JavaScript дилетант Урок 1: Стрелки функции это именно то, что, черт возьми?
- Углубленный урок JavaScript 2: Что означает, что функция должна быть гражданином первого класса?
- Урок 3 по углубленному изучению JavaScript: что такое алгоритм сборки мусора?
- Подробный урок JavaScript 4: Как работает движок V8?
- Урок 5 по углубленному изучению JavaScript: как Chrome преуспел?
Недавно в экосистеме JavaScript появилось еще 2 очень хардкорных проекта.
Великий богFabrice BellardВыпустил новый двигатель JSQuickJS, вы можете преобразовать исходный код JavaScript в код языка C, а затем использовать системный компилятор (gcc или clang) для создания исполняемого файла.
Facebook разрабатывает новый JS-движок для React NativeHermes, который используется для оптимизации производительности на стороне Android. Он может компилировать исходный код JavaScript в байт-код при создании приложения, тем самым уменьшая размер APK, уменьшая использование памяти и повышая скорость запуска приложения.
Как программист JavaScript, лишь немногие люди имеют возможность и способность реализовать движок JS, но все же необходимо понимать движок JS. В этой статье мы познакомим вас с принципом работы двигателя V8 в надежде помочь вам.
JavaScript-движок
Когда код JavaScript, который мы пишем, напрямую отправляется в браузер или узел для выполнения, базовый ЦП не знает об этом и не может его выполнить. ЦП знает только свой собственный набор инструкций, и этот набор инструкций соответствует ассемблерному коду. Писать код на ассемблере очень сложно, например, если мы хотим вычислить факториал N, нам нужно всего 7 строк рекурсивной функции:
function factorial(N) {
if (N === 1) {
return 1;
} else {
return N * factorial(N - 1);
}
}
Логика кода также очень ясна, что полностью согласуется с математическим определением факториала, и ее могут понять даже люди, не умеющие писать код.
Однако, если вы используете язык ассемблера для написания N факториала, это займет более 300 строк кода.n-factorial.s:
Ассемблерный код этого факториала N был написан мной, когда я учился в колледже. Это было уже N лет назад. Он должен иметь дело с преобразованием десятичных и двоичных чисел и должен использовать несколько байтов для хранения больших целых чисел. Он может вычислять примерно до 500 Н факториала слева и справа.
Еще один момент заключается в том, что наборы инструкций разных типов ЦП отличаются, а это означает, что ассемблерный код приходится переписывать для каждого типа ЦП, что очень сильно дает сбои. . .
отлично,Механизм JavaScript может компилировать код JS в код сборки, соответствующий различным процессорам (Intel, ARM, MIPS и т. д.)., так что нам не нужно просматривать руководство по набору инструкций для каждого процессора. Конечно, работа движка JavaScript заключается не только в компиляции кода, он также отвечает за выполнение кода, выделение памяти ивывоз мусора.
Хотя браузеров много, основных движков JavaScript на самом деле очень мало, ведь разработка движка JavaScript — очень сложная вещь. Наиболее известные JS-движки:
- V8 (Google)
- SpiderMonkey (Mozilla)
- JavaScriptCore (Apple)
- Chakra (Microsoft)
- Интернет вещей:duktape,JerryScript
Кроме того, недавно выпущенныйQuickJSа такжеHermesЭто тоже движок JS, они вышли из категории браузеров,Atwood's LawЕще раз доказали:
Any application that can be written in JavaScript, will eventually be written in JavaScript.
V8: Мощный движок JavaScript
Среди нескольких движков JavaScript, V8, несомненно, является самым популярным.И Chrome, и Node.js используют движок V8.Доля Chrome на рынке достигает 60%, а Node.js является стандартом де-факто для внутреннего программирования JS. Многие отечественные браузеры на самом деле разработаны на основе браузера Chromium, а Chromium эквивалентен версии Chrome с открытым исходным кодом, которая, естественно, основана на движке V8. Чудесным образом даже Microsoft, уникальная компания в мире браузеров, присоединилась к лагерю Chromium. Кроме того, Electron основан на Node.js и Chromium для разработки настольных приложений, а также на базе V8.
Двигатель V8 был выпущен в 2008 году. Его название было вдохновлено двигателем V8 автомобилей с высокими характеристиками. Требуется некоторая сила, чтобы осмелиться назвать его. Его производительность постоянно улучшается. Ниже приводится использованиеSpeedometer benchmarkРезультат теста:
Источник изображения:v8.dev/V8 был очень успешным в отрасли, и он также был подтвержден академией и выиграл ACM SigplanProgramming Languages Software Award:
V8's success is in large part due to the efficient machine code it generates. Because JavaScript is a highly dynamic object-oriented language, many experts believed that this level of performance could not be achieved. V8's performance breakthrough has had a major impact on the adoption of JavaScript, which is nowadays used on the browser, the server, and probably tomorrow on the small devices of the internet-of-things.
JavaScript — язык с динамической типизацией, что увеличивает сложность компилятора, поэтому эксперты считают, что его производительность сложно улучшить, но V8 действительно сделал это, сгенерировав очень эффективный машинный код (фактически ассемблерный код), который позволяет применять JS. в различных областях, таких как Интернет, приложение, рабочий стол, сервер и Интернет вещей.
Строго говоря, код, сгенерированный V8, представляет собой ассемблерный код, а не машинный код, но в документации, блогах и других источниках, связанных с V8, код, сгенерированный V8, называется машинным кодом. Многие ассемблерный код и машинный код находятся во взаимно однозначном соответствии, и их легко конвертировать друг в друга.Это тоже принцип декомпиляции.Поэтому не лишено смысла называть ими код сгенерированный V8 Machine Кодекс, но он не строгий.
Внутренняя структура двигателя V8
V8очень сложный проект, использующийclocСтатистика показывает, что онаБолее 1 миллиона строк кода C++.
V8 состоит из множества подмодулей, из которых эти 4 модуля являются наиболее важными:
- Parser: Отвечает за преобразование исходного кода JavaScript в абстрактное синтаксическое дерево (AST).
- Ignition: Интерпретатор, интерпретатор, отвечает за преобразование AST в байт-код, интерпретацию и выполнение байт-кода, в то же время сбор информации, необходимой для оптимизации и компиляции TurboFan, такой как тип параметров функции;
- TurboFan: компилятор, компилятор использует информацию о типе, собранную Ignitio, для преобразования байт-кода в оптимизированный ассемблерный код;
- Orinoco:уборщик мусора,вывоз мусораМодуль отвечает за освобождение памяти, которая больше не нужна программе;
Среди них Parser, Ignition и TurboFan могут компилировать исходный код JS в ассемблерный код, блок-схема выглядит следующим образом:
Проще говоря, Parser преобразует исходный код JS в AST, затем Ignition преобразует AST в байт-код, и, наконец, TurboFan преобразует байт-код в оптимизированный машинный код (фактически ассемблерный код).
- Если функция не вызывается, V8 не будет ее компилировать.
- Если функция вызывается только один раз, Ignition компилирует ее в байт-код и напрямую интерпретирует. TurboFan не компилируется с оптимизацией, поскольку требует от Ignition сбора информации о типе при выполнении функции. Это требует, чтобы функция была выполнена хотя бы один раз, прежде чем TurboFan сможет выполнить оптимизированную компиляцию.
- Если функция вызывается несколько раз, она может быть идентифицирована как функция горячей точки, и собранная информация о типе Ignition может быть оптимизирована.В это время TurboFan компилирует байт-код в оптимизированный машинный код, чтобы повысить производительность выполнения кода.
Красная линия на картинке обратная, это действительно странно, Оптимизированный Машинный Код будет сведен к Байткоду, процесс называется Деоптимизация. Это связано с тем, что информация, собранная Ignition, может быть неверной, например, добавить параметры перед функцией в целое число, а позже превратить в строку. Оптимизированная функция генерации машинного кода предполагала, что параметр является целым числом, конечно, ложным, тогда требуется деоптимизация.
function add(x, y) {
return x + y;
}
add(1, 2);
add("1", "2");
Перед запуском таких программ, как C, C++ и Java, их необходимо скомпилировать, и исходный код нельзя выполнить напрямую, но для JavaScript мы можем напрямую выполнить исходный код (например, node server.js), который скомпилирован. , а затем выполняется при запуске, этот метод называется JIT-компиляцией, для краткости. Следовательно, V8 также относится к JIT-компилятору.
Зажигание: Переводчик
Node.js реализован на базе движка V8, поэтому команда node предоставляет множество параметров для движка V8, используя--print-bytecode
Возможность распечатать байт-код, сгенерированный Ignition.
factorial.js выглядит следующим образом, поскольку V8 не будет компилировать функции, которые не вызываются, функцию factorial нужно вызывать в последней строке.
function factorial(N) {
if (N === 1) {
return 1;
} else {
return N * factorial(N - 1);
}
}
factorial(10); // V8不会编译没有被调用的函数,因此这一行不能省略
С помощью команды узла (версия узла 12.6.0)--print-bytecode
Опции, распечатывание Bytecode зажигание сгенерировано:
node --print-bytecode factorial.js
Консольный вывод очень большой, последняя часть — это байт-код функции factorial:
[generated bytecode for function: factorial]
Parameter count 2
Register count 3
Frame size 24
18 E> 0x3541c2da112e @ 0 : a5 StackCheck
28 S> 0x3541c2da112f @ 1 : 0c 01 LdaSmi [1]
34 E> 0x3541c2da1131 @ 3 : 68 02 00 TestEqualStrict a0, [0]
0x3541c2da1134 @ 6 : 99 05 JumpIfFalse [5] (0x3541c2da1139 @ 11)
51 S> 0x3541c2da1136 @ 8 : 0c 01 LdaSmi [1]
60 S> 0x3541c2da1138 @ 10 : a9 Return
82 S> 0x3541c2da1139 @ 11 : 1b 04 LdaImmutableCurrentContextSlot [4]
0x3541c2da113b @ 13 : 26 fa Star r1
0x3541c2da113d @ 15 : 25 02 Ldar a0
105 E> 0x3541c2da113f @ 17 : 41 01 02 SubSmi [1], [2]
0x3541c2da1142 @ 20 : 26 f9 Star r2
93 E> 0x3541c2da1144 @ 22 : 5d fa f9 03 CallUndefinedReceiver1 r1, r2, [3]
91 E> 0x3541c2da1148 @ 26 : 36 02 01 Mul a0, [1]
110 S> 0x3541c2da114b @ 29 : a9 Return
Constant pool (size = 0)
Handler Table (size = 0)
Сгенерированный байт-код на самом деле довольно прост:
- Используйте команду LdaSmi, чтобы сохранить целое число 1 в регистр;
- Используйте команду TestEqualStrict, чтобы сравнить размер параметров a0 и 1;
- Если a0 равно 1, команда JumpIfFalse не будет переходить и продолжит выполнение следующей строки кода;
- Если a0 не равно 1, команда JumpIfFalse переходит к адресу памяти 0x3541c2da1139.
- ...
Нетрудно обнаружить, что Bytecode в какой-то степени является языком ассемблера, но он не соответствует конкретному процессору или соответствует виртуальному процессору. Таким образом, гораздо проще генерировать байт-код, и нет необходимости генерировать разные коды для разных процессоров. Вы должны знать, что V8 поддерживает 9 различных ЦП, а введение байт-кода промежуточного уровня может упростить процесс компиляции V8 и улучшить масштабируемость.
Если мы сгенерируем байт-код на другом оборудовании, мы обнаружим, что инструкции для генерации кода одинаковы:
Источник изображения:Ross McIlroyТурбофан: Компилятор
с помощью команды узла--print-code
так же как--print-opt-code
возможность распечатать ассемблерный код, сгенерированный TurboFan:
node --print-code --print-opt-code factorial.js
Я работаю на Mac, и результат выглядит так:
По сравнению с байт-кодом настоящий ассемблерный код гораздо менее читаем. Более того, если тип ЦП машины отличается, сгенерированный ассемблерный код также отличается.
Этот ассемблерный код оставляем в покое, потому что важнее всего понять, как TurboFan оптимизирует сгенерированный ассемблерный код. Мы можем использовать функцию добавления, чтобы разобраться во всем процессе оптимизации.
function add(x, y) {
return x + y;
}
add(1, 2);
add(3, 4);
add(5, 6);
add("7", "8");
Поскольку переменные JS не имеют типа, параметры функции add могут быть любого типа: Number, String, Boolean и т. д., что означает, что функция add может добавлять числа (V8 также различает целые числа и числа с плавающей запятой), это может быть конкатенация строк и, возможно, другие более сложные операции. Если вы скомпилируете его напрямую, сгенерированный код будет иметь много ветвей if...else Псевдокод выглядит следующим образом:
if (isInteger(x) && isInteger(y)) {
// 整数相加
} else if (isFloat(x) && isFloat(y)) {
// 浮点数相加
} else if (isString(x) && isString(y)) {
// 字符串拼接
} else {
// 各种其他情况
}
Я написал только 4 ветки, но на самом деле ветвей больше.Например, когда типы параметров несовместимы, необходимо выполнить преобразование типов.Вы можете посмотреть, как ECMASCRipt определяет сложение:12.8.3The Addition Operator ( + ).
Если ассемблерный код генерируется непосредственно по псевдокоду, сгенерированный код должен быть очень многословным, что займет много места в памяти.
Зажигание выполняетсяadd(1, 2)
Когда известно, что два параметра функции добавления являются целыми числами, когда TurboFan компилирует байт-код, он может предположить, что параметры функции добавления являются целыми числами, что может значительно упростить сгенерированный ассемблерный код.Псевдокод выглядит следующим образом:
if (isInteger(x) && isInteger(y)) {
// 整数相加
} else {
// Deoptimization
}
Конечно, делать это тоже рискованно, потому что, если параметр функции добавления не является целым числом, сгенерированный ассемблерный код не может быть выполнен, и его можно выполнить только с помощью Deoptimize as Bytecode.
То есть, если TurboFan компилирует и оптимизирует функцию добавления, тоadd(3, 4)
а такжеadd(3, 4)
может выполнять оптимизированный ассемблерный код, ноadd("7", "8")
Только Deoptimize может быть выполнен как байт-код.
Конечно, TurboFan не только упрощает поток выполнения кода на основе информации о типе, но и выполняет другие оптимизации, такие как сокращение избыточного кода и другие более сложные вещи.
Из этого простого примера видно, что если изменить тип переменных в нашем JS-коде, это добавит много проблем движку V8.Чтобы улучшить производительность, мы можем попробовать не менять тип переменных.
Для проектов с высокими требованиями к производительности использование TypeScript также является хорошим выбором.Теоретически, если строго следовать методу типизированного программирования, производительность также может быть улучшена.Конечно, типизированный код полезен для движка V8 для оптимизации скомпилированного кода сборки эти тестовые данные также необходимы, чтобы доказать это.
Ориноко: Сбор мусора
Мощная функция сборки мусора является одним из ключевых факторов повышения производительности V8, поскольку она может освобождать место в памяти и повышать эффективность использования памяти, не влияя на выполнение кода JS.
Что касается вывоза мусора, я вУрок 3 по углубленному изучению JavaScript: что такое алгоритм сборки мусора?Есть подробные введения, поэтому я не буду повторять их здесь.
Будущее JS-движков
Двигатель V8 действительно очень мощный, но он не всесилен, и простой анализ позволяет найти некоторые моменты, которые можно оптимизировать.
У меня есть новая идея, я еще не придумал название, назовем ее Optimized TypeScript Engine:
- Используйте программирование на TypeScript, следуйте строгим правилам типизированного программирования, не пишите AnyScript;
- При сборке TypeScript напрямую компилируется в байт-код, а не генерирует JS-файлы, поэтому при запуске процесс генерации синтаксического анализа и байт-кода опускается;
- При запуске байт-код необходимо сначала скомпилировать в ассемблерный код, соответствующий процессору;
- Благодаря использованию типизированного программирования компилятору выгодно оптимизировать сгенерированный ассемблерный код и сэкономить множество дополнительных операций;
Эта идея действительно может быть реализована на базе двигателя V8, и она должна быть технически осуществимой:
- Разделите Parser и Ignition на этапе сборки;
- Удалите соответствующий код TurboFan, связанный с динамическими функциями JS;
Таким образом, движок JS можно сильно упростить: с одной стороны, ему больше не нужно парсить и генерировать байт-код, а с другой стороны, компилятору больше не нужно делать много лишней работы из-за динамического характеристики JavaScript. Таким образом, вы можете уменьшить использование ЦП, памяти и энергии, оптимизировать производительность, единственная проблема может заключаться в том, что вы должны использовать строгий синтаксис TS для программирования.
почему ты хочешь сделать это? Потому что для оборудования Интернета вещей необходимо экономить ЦП, память и электроэнергию.Не каждая умная бытовая техника должна быть оснащена Snapdragon 855. Если вы хотите применить JS к области Интернета вещей, вы должны исходить из точки зрения JS движок.Для оптимизации бесполезно просто делать фреймворк верхнего уровня.
На самом деле, FacebookHermesЭто в значительной степени то, что он делает, за исключением того, что он не требует программирования с TS.
Это должно быть будущее движка JS, все будут видеть все больше и больше таких тенденций.
Что касается JS, я планирую потратить 1 год на написание серии блогов**"Простое объяснение JavaScript"**, в чем еще ты не уверен? Не стесняйтесь оставлять сообщение, я могу изучить его, а затем поделиться им с вами. Добро пожаловать, чтобы добавить мой личный WeChat (KiwenLau), яFundebugТехнический директор, программист, который любит и ненавидит JS.
Ссылаться на
- Celebrating 10 years of V8
- Launching Ignition and TurboFan
- JavaScript engines - how do they even?
- An Introduction to Speculative Optimization in V8
- Понимание байт-кода V8
- Что случилось с JavaScript в 2018 году?
- Урок 3 по углубленному изучению JavaScript: что такое алгоритм сборки мусора?
- Какой уровень программиста у Фабриса Белларда?
- Как вы оцениваете движок QuickJS JS, выпущенный Фабрисом Белларом?
О Фундебаге
FundebugСосредоточьтесь на JavaScript, апплете WeChat, мини-игре WeChat, апплете Alipay, React Native, Node.js и мониторинге ошибок онлайн-приложений Java в режиме реального времени. С момента официального запуска Double Eleven в 2016 году Fundebug обработала в общей сложности более 1 миллиарда ошибок, включая Sunshine Insurance, Walnut Programming, Lizhi FM, Head 1:1, Weimai, Youth League Club и многие другие бренды. Бесплатная пробная версия приветствуется!
Уведомление об авторских правах
Пожалуйста, указывайте автора при перепечаткеFundebugИ адрес этой статьи:bug.com/2019/07/16/…