До написания этой статьи я ни разу не задавал себе этот вопрос в своей работе, не то что при написании кода, компилятор редактирует код в 01 код, который может распознать компьютер, что тут понимать?
На самом деле, компилятор в коде JS становятся исполняемым кодом, делают много сложной работы, только углубляя понимание принципов подбора компиляции, мы можем суть позади различных внешних рамок для записи лучшего понимания кода.
Чтобы написать эту статью, редактор также очень напуган.После прочтения соответствующей информации, это также процесс обучения и понимания.Неизбежно, что будут некоторые проблемы.Вы можете исправить меня и улучшить вместе.
Отступление - возвращение к детской любознательности
Нынешний темп жизни и давление могут заставить нас затаить дыхание.Мы пишем код день за днем и устали от изучения различных интерфейсных фреймворков.Скорость обучения всегда не поспевает за скоростью обновления, и мы часто находим решения к проблемам или проблемам.Лучший способ исправить ошибки, но очень мало времени, чтобы действительно остановиться и изучить наш самый основной инструмент - язык JavaScript.
Не знаю, все ли еще помнят свое детство, увидев обновку или игрушку, есть ли у них сильное любопытство, и им приходится разбивать запеканку и просить тебя до конца. Однако, в нашей работе, есть ли у вас сильное любопытство к различным встречающимся проблемам кода? Чтобы узнать, вы должны добавить эти проблемы в "черный список", и они вам не понадобятся в следующий раз. Я не знаю, почему.
На самом деле, мы должны вернуться в детство, недовольные использованием, просто дайте коду работать, мы должны понять «почему», только тогда вы сможете охватить весь JavaScript. Освоив эти знания, вы сможете легко разобраться в любой технологии или фреймворке, поэтому публичный аккаунт фронтенда обновляет основы JavaScript.
Не путайте JavaScript с браузерами
Язык и среда — два разных понятия. Когда дело доходит до JavaScript, большинство людей, вероятно, думают о браузерах, а JavaScript невозможно запустить без браузера, который сильно отличается от других языков системного уровня. Например, на языке C можно разрабатывать системы и производственные среды, а JavaScript может работать только в определенной среде.
Среда выполнения JavaScript обычно имеет хост-среду и среду выполнения. Как показано ниже:
Среда хоста создается оболочкой Например, браузер является средой оболочки (но браузер не единственный, многие серверы и системы настольных приложений также могут предоставлять среду, в которой работает механизм JavaScript). Среда периода выполнения создается движком JavaScript (например, движком V8, который будет подробно описан позже), встроенным в оболочку.В этой среде периода выполнения сначала необходимо создать начальную среду для разбора кода. содержание включает в себя:
- Набор правил, связанных со средой хостинга
- Ядро движка JavaScript (основные правила синтаксиса, логика, команды и алгоритмы)
- Набор встроенных объектов и API
- Другие соглашения
Хотя различные определения двигателя JavaScript отличаются, это сформировало так называемые проблемы совместимости браузера, потому что разные браузеры используют разные двигатели Javancip.
Но последние новости должны быть известны всем — на рынке браузеров Microsoft фактически отказалась от собственного EDGE (преемника IE) и обратилась к ядру Chromium, где доминирует конкурент Google (отечественные браузеры Baidu, Sogou, Tencent, Cheetah, UC , Maxthon и 360 все используют Chromium (Chromium использует знаменитый двигатель V8, все должны его хорошо знать), можно считать, что все они - жилеты Chromium), что действительно интересно, мы наконец-то счастливы в одной среде. приятно думать о написании кода!
Пересмотрите принцип компиляции
Когда дело доходит до языка JavaScript, большинство людей классифицируют его как «динамический» или «интерпретируемый язык выполнения», но на самом деле это «компилируемый» язык. В отличие от традиционных компилируемых языков, он не компилируется заранее, а скомпилированные результаты не переносятся между распределенными системами. Прежде чем представить принцип компилятора JavaScript, мы с Xiaobian рассмотрим основной принцип компилятора, поскольку он является самым основным, и мы можем лучше понять компилятор JavaScript.
Общие этапы компиляции программы делятся на:Лексический анализ, синтаксический анализ, семантическая проверка, оптимизация кода и генерация байт-кода.
Конкретный процесс компиляции выглядит следующим образом:
Слово/лексический анализ (Tokenizing/Lexing)
Так называемое словное сегментацию похоже на разделить предложение в соответствии с наименьшим блоком слова. Перед составлением куска кода компьютер также разбирается в строку кода в значимые кодовые блоки, которые называютсяЛексическая единица (токен).
Например, рассмотрим программуvar a=2
. Эта программа обычно разбивается на следующие лексические единицы:var
,a
,=
,2
; Считается ли пробел лексической единицей, зависит от того, имеет ли пробел значение в языке.
Разбор/Разбор
Этот процессПреобразует поток лексических единиц в дерево вложенных элементов, представляющих грамматическую структуру программы. Это дерево называется «Абстрактное синтаксическое дерево» (Abstract Syntax Tree, AST).
Лексический анализ и синтаксический анализ не являются полностью независимыми, а чередуются, то есть лексический анализатор не использует синтаксический анализатор после прочтения всех лексем. При нормальных обстоятельствах каждый раз, когда лексическая лексема получена, она отправляется парсеру для анализа.
Процесс синтаксического анализа заключается в создании синтаксического дерева из токенов, сгенерированных в результате лексического анализа, а с точки зрения непрофессионала — в сохранении информации, полученной из программы, в структуру данных.Обратите внимание, что при компиляции используются две структуры данных: таблица символов и синтаксическое дерево.
Таблица символов: таблица, используемая для хранения всех символов в программе, включая все строковые переменные, литеральные строки, а также функции и классы.
Синтаксическое дерево: древовидное представление структуры программы, используемое для генерации промежуточного кода. Ниже приведена простая условная структура и фрагмент кода выходной информации после преобразования синтаксического анализатора в синтаксическое дерево, например:
if (typeof a == "undefined") {
a = 0;
} else {
a = a;
}
alert(a);
Если интерпретатор JavaScript обнаружит, что его невозможно построить при построении синтаксического дерева, он сообщит о синтаксической ошибке и завершит синтаксический анализ всего блока кода. Для традиционных строго типизированных языков после построения синтаксического дерева синтаксическим анализом переведенные предложения могут оставаться неоднозначными и требовать дальнейшей семантической проверки.
**Основной частью семантической проверки является проверка типов. ** Например, совпадают ли фактические и формальные типы параметров функции. Однако для слаботипизированных языков такого шага нет.
После подготовки к этапу компиляции код JavaScript создается в виде синтаксического дерева в памяти, а затем механизм JavaScript интерпретирует и выполняет его в соответствии со структурой синтаксического дерева.
генерация кода
Процесс преобразования AST в исполняемый код называется генерацией кода. Этот процесс зависит от языка и целевой платформы.
После понимания принципа компиляции, на самом деле, движок JavaScript намного сложнее, потому что в большинстве случаев процесс компиляции JavaScript происходит не до построения, а происходит за несколько микросекунд до выполнения кода или даже в более короткие сроки. время. Чтобы обеспечить наилучшую производительность, JavaScipt использует различные методы, которые будут подробно описаны позже.
Таинственный компилятор JavaScipt — движок V8
Поскольку большинство JavaScipt работает в браузерах, движки, используемые разными браузерами, также различаются. Ниже приведены текущие основные движки браузеров:
В связи с появлением компилятора Google V8 он привлек значительное внимание благодаря своей хорошей производительности.Официально из-за появления V8 наш нынешний интерфейс может сиять, и распускаться сотни цветов.Движок V8 написан на C++ , Как движок JavaScript, он изначально использовался для браузера Google Chrome. Он был выпущен и открыт с первой версией Chrome. Теперь у него есть много других пользователей помимо браузера Chrome. Например, NodeJS, MongoDB, CouchDB и т. д.
Самая захватывающая новость последнего времени заключается в том, что Microsoft фактически отказалась от собственного EDGE (преемника IE) и обратилась к ядру Chromium, в котором доминирует конкурент Google (отечественные браузеры Baidu, Sogou, Tencent, Cheetah, UC, Maxthon, 360 All из них используют Chromium (Chromium использует знаменитый двигатель V8, все должны его очень хорошо знать). Похоже, что двигатель V8 будет доминировать в реках и озерах в ближайшем будущем. Следующий редактор сосредоточится на двигателе V8.
Когда V8 компилирует код JavaScript,Парсер (парсер)Будет генерировать А.абстрактное синтаксическое деревоБайтовый код (Bytecode)Машинный код.
Раньше у V8 было два компилятора
До версии 5.9 движок использовал два компилятора:
full-codegen — простой и быстрый компилятор, генерирующий простой и относительно медленный машинный код.
Коленчатый вал - более сложный (просто-временный) оптимизирующий компилятор, который производит высоко оптимизированный код.
Движок V8 также использует несколько потоков внутри:
- Основной поток: получить код, скомпилировать код и выполнить его
- Оптимизированный поток: параллельно основному потоку для оптимизации генерации кода.
- Поток Profiler: он сообщит среде выполнения, на какие методы мы тратим много времени, чтобы Crankshaft мог их оптимизировать.
- некоторые другие потоки для обработки сканирования сборщика мусора
байт-код
Байт-код — это абстракция машинного кода. Компиляция байт-кода в машинный код упрощается, если байт-код разработан с использованием той же вычислительной модели, что и физический ЦП. Вот почему интерпретаторы часто представляют собой регистры или стеки. Зажигание - это регистр с аккумулятором.
Вы можете думать о байт-кодах V8 как о небольших строительных блоках (байт-кодах), которые объединяются для формирования любой функциональности JavaScript. V8 имеет сотни байт-кодов. Операторы, такие как Add или TypeOf, или загрузчики свойств, такие как LdaNamedProperty, и многие подобные байт-коды. V8 также имеет несколько очень специальных байт-кодов, таких как CreateObjectLiteral или SuspendGenerator. Заголовочный файл bytescodes.h (GitHub.com/V8/V8/blob/…Определяет полный список байт-кодов V8.
В раннем движке V8 большинство браузеров были основаны на байт-коде. Движок V8 пропустил этот шаг и напрямую компилировал jS в машинный код. Причина этого заключалась в экономии времени и повышении эффективности, но позже обнаружилось, что он занимает слишком много памяти. Наконец, вернемся к байт-коду, какова мотивация для этого?
- Уменьшите объем памяти, занимаемый машинным кодом, то есть пожертвуйте временем ради места. (основной мотив)
- Улучшение скорости запуска кода кода v8 реконструируется.
- Уменьшите сложность кода v8.
Каждый байт-код определяет свой ввод и вывод как регистровые операнды. Зажигание использует регистры r0, r1, r2, ... и регистры-аккумуляторы. Почти все байт-коды используют регистры-аккумуляторы. Это похоже на обычный регистр, за исключением того, что байт-код не определяет его. Например, Add r1 добавляет значение в регистре r1 к значению в аккумуляторе. Это делает байт-код короче и экономит память.
Многие байт-коды начинаются с Lda или Sta. А в Lda и Stastands является аккумулятором. Например, LdaSmi[42] загружает небольшое целое число (Smi) 42 в регистр-накопитель. Звезда r0 сохраняет значение, которое в настоящее время находится в аккумуляторе в регистре r0.
С основами, которые у вас есть сейчас, найдите минутку, чтобы взглянуть на байт-код с реальной функциональностью.
function incrementX(obj) {
return 1 + obj.x;
}
incrementX({ x: 42 }); // V8 的编译器是惰性的,如果一个函数没有运行,V8 将不会解释它
Если вы хотите увидеть байт-код JavaScript V8, вы можете использовать параметры командной строки, чтобы добавить--print-bytecode
Запустите D8 или Node.js (8.3 или более позднюю версию) для печати. Для Chrome запустите Chrome из командной строки, используя--js-flags="--print-bytecode"
Пожалуйста, обратитесь к запуску Chromium с флагами.
$ node --print-bytecode incrementX.js
...
[generating bytecode for function: incrementX]
Parameter count 2
Frame size 8
12 E> 0x2ddf8802cf6e @ StackCheck
19 S> 0x2ddf8802cf6f @ LdaSmi [1]
0x2ddf8802cf71 @ Star r0
34 E> 0x2ddf8802cf73 @ LdaNamedProperty a0, [0], [4]
28 E> 0x2ddf8802cf77 @ Add r0, [6]
36 S> 0x2ddf8802cf7a @ Return
Constant pool (size = 1)
0x2ddf8802cf21: [FixedArray] in OldSpace
- map = 0x2ddfb2d02309 <Map(HOLEY_ELEMENTS)>
- length: 1 0: 0x2ddf8db91611 <String[1]: x>
Handler Table (size = 16)
Мы игнорируем большую часть вывода и фокусируемся на фактическом байт-коде.
Вот что означает каждый байт-код, каждая строка:
LdaSmi [1]
Star r0
Затем Star r0 сохраняет значение 1, которое в настоящее время находится в аккумуляторе в регистре r0.
LdaNamedProperty a0, [0], [4]
LdaNamedProperty загружает именованное свойство a0 в аккумулятор. ai указывает на i-й параметр incrementX() . В этом примере мы ищем именованное свойство в a0, которое является первым аргументом для incrementX(). Имя свойства определяется константой 0. LdaNamedProperty использует 0 для поиска имени в отдельной таблице:
- length: 1
0: 0x2ddf8db91611 <String[1]: x>
Как видите, 0 сопоставляется с x. Итак, эта строка байт-кода означает загрузку obj.x.
Тогда значение 4 операнда делает это? Это функция incrementX() векторного индекса обратной связи. Vector содержит информацию об обратной связи во время выполнения для оптимизации производительности.
Теперь реестр выглядит так:
Add r0, [6]Последняя инструкция добавляет r0 к аккумулятору, в результате чего получается 43. 6 — еще один индекс вектора обратной связи.
Return Возвращает значение в аккумуляторе. Оператор return является концом функции incrementX(). В этот момент вызывающая функция incrementX() может получить значение 43 в аккумуляторе и продолжить обработку этого значения.
Почему двигатель V8 такой быстрый?
Благодаря характеристикам слабого языка JavaScript (переменной могут быть присвоены разные типы данных) и гибкости, позволяющей нам добавлять или удалять свойства и методы объекта в любое время, язык JavaScript очень динамичен, и мы можем представить, что он значительно увеличитсяСложность компиляции двигателя, хотя это очень сложно, не сложно для двигателя V8.Двигатель v8 использует несколько технологий для достижения цели ускорения:
Встраивание:
Функция встраивания является основой всех оптимизаций и имеет решающее значение для хорошей производительности. Так называемый встроен означает, что если функция внутренне вызывает другие функции, компилятор будет непосредственно заменить метод функции с содержимым выполнения в функции. Как показано ниже:
Как понять это? См. следующий код
function add(a, b) {
return a + b;
}
function calculateTwoPlusFive() {
var sum;
for (var i = 0; i <= 1000000000; i++) {
sum = add(2 + 5);
}
}
var start = new Date();
calculateTwoPlusFive();
var end = new Date();
var timeTaken = end.valueOf() - start.valueOf();
console.log("Took " + timeTaken + "ms");
Благодаря функции встроенных атрибутов перед компиляцией код будет оптимизирован в
function add(a, b) {
return a + b;
}
function calculateTwoPlusFive() {
var sum;
for (var i = 0; i <= 1000000000; i++) {
sum = 2 + 5;
}
}
var start = new Date();
calculateTwoPlusFive();
var end = new Date();
var timeTaken = end.valueOf() - start.valueOf();
console.log("Took " + timeTaken + "ms");
Можете ли вы представить, насколько медленным это было бы без функции встроенных свойств? Вставляем первый кусок JS-кода в HTML-файл, открываем его в разных браузерах (аппаратное окружение: i7, память 16G, система Mac) и открываем сафари, как показано на рисунке ниже, 17 секунд:
Если вы откроете его в Chrome, это займет менее 1 секунды, на 16 секунд быстрее!
Скрытый класс:
Например, каждая переменная в языке со статической типизацией, таком как C++/Java, имеет уникальный тип. Поскольку информация о типе, какие члены содержит объект, и смещения этих членов в объекте могут быть определены на этапе компиляции, процессору нужно использовать только первый адрес объекта при выполнении - в C++ это указатель плюс член Внутренние члены могут быть доступны по смещениям внутри объекта. Эти инструкции доступа генерируются на этапе компиляции.
Но для динамического языка, такого как JavaScript, переменные могут быть назначены объектами разных типов в любое время во время выполнения, а сами объекты могут добавлять или удалять элементы в любое время. Информация, необходимая для доступа к свойствам объекта, полностью определяется средой выполнения. Чтобы получить доступ к членам по индексу, V8 «по-тихому» классифицирует запущенные объекты, а в процессе генерирует структуру данных внутри V8, то есть скрытый класс. Сам скрытый класс является объектом.
Рассмотрим следующий код:
function Point(x, y) {
this.x = x;
this.y = y;
}
var p1 = new Point(1, 2);
еслиnew Point(1, 2)
вызывается, движок v8 создаст скрытый классC0
,Как показано ниже:
Поскольку Point не имеет никаких свойств, поэтомуC0
Пусто
однаждыthis.x = x
При выполнении движок v8 создает второй скрытый класс с именем «C1». На основе "c0" "c1" описывает место (эквивалентно указателю) в памяти, где можно найти атрибут X. В этом случае скрытый класс будет изменен сC0
переключиться наC1
,Как показано ниже:
Каждый раз, когда к объекту добавляется новое свойство, старый скрытый класс переключается на новый скрытый класс посредством перехода пути. Из-за важности преобразований, поскольку движок позволяет создавать объекты таким же образом, чтобы совместно использовать скрытые классы. Если два объекта имеют общий скрытый класс и вы добавляете одинаковые свойства к обоим объектам, процесс преобразования гарантирует, что оба объекта будут использовать один и тот же скрытый класс со всеми оптимизациями кода.
при исполненииthis.y = y
, создастC2
, скрытый класс меняется наC2
.
Производительность преобразования скрытых классов зависит от порядка добавления атрибутов.Если порядок добавления другой, эффект будет другим, как показано в следующем коде:
function Point(x, y) {
this.x = x;
this.y = y;
}
var p1 = new Point(1, 2);
p1.a = 5;
p1.b = 6;
var p2 = new Point(3, 4);
p2.b = 7;
p2.a = 8;
Вы можете подумать, что P1 и p2 используют одни и те же скрытые классы и преобразования, но это не так. Для объекта P1 скрытый класс сначала будет a, а затем b. Для p2 скрытый класс будет сначала b, а затем a. В конце концов будут сгенерированы разные скрытые классы, что увеличит вычислительные затраты на компиляцию. следует использовать тот же порядок Динамически изменять свойства объекта, чтобы можно было повторно использовать скрытые классы.
Встроенное кэширование
- Обычный процесс доступа к свойствам объекта: сначала получить адрес скрытого класса, затем найти значение смещения в соответствии с именем свойства, а затем вычислить адрес свойства. Хотя объем работы по поиску во всей среде выполнения значительно сократился по сравнению с прошлым, он по-прежнему занимает много времени. Можно ли кэшировать результаты предыдущего запроса для повторного доступа? Конечно можно, это встроенный кеш.
- Общая идея встроенного кеша состоит в том, чтобы сохранить скрытый класс и значение смещения начального поиска.При выполнении следующего поиска сначала сравнить, является ли текущий объект предыдущим скрытым классом.Если да, то использовать предыдущий результат кеша непосредственно, чтобы уменьшить количество времени для поиска в таблице. Конечно, если у объекта несколько атрибутов, вероятность ошибок кеша возрастет, так как после изменения типа определенного атрибута изменится и скрытый класс объекта, который не согласуется с предыдущим кешем и требует поиска предыдущим способом хеш-таблица.
управление памятью
Группа по управлению памятью должна бытьраспространятьа такжеПерерабатыватьСостоит из двух частей. Разделение памяти V8 выглядит следующим образом:
- Зона: управление небольшой памятью. Его собственное приложение для первой памяти, а затем управлять и выделять небольшую память, когда выделяется небольшая память и не может быть восстановлена зона, только единовременное выделение всей небольшой памяти зоны рециркуляции. Когда процессу требуется много памяти, Zone должен будет выделить много памяти, но не сможет вовремя восстановиться, что приведет к проблемам с памятью.
- Куча: управляет данными, используемыми JavaScript, сгенерированным кодом, хеш-таблицами и т. д. Для облегчения сборки мусора куча делится на три части:
1. Молодое поколение: Выделите пространство памяти для вновь созданных объектов, часто требующих сборки мусора. Чтобы облегчить утилизацию контента в молодого поколения, молодое поколение можно разделить на две половины, одна половина используется для распределения, а другая половина отвечает за копирование объектов, которые необходимо сохранить до во время переработки.
2. Старое поколение: Сохраняйте старые объекты, указатели, код и другие данные по мере необходимости с меньшим объемом сбора мусора.
3. Большие объекты: выделять память для тех объектов, которым требуется больше памяти.Конечно, это может также включать память, выделенную для данных и кода.Для страницы выделяется только один объект.
вывоз мусора
V8 использует выделение памяти для генерации и больших данных.При высвобождении памяти он использует упрощенный алгоритм сортировки, чтобы пометить объекты, на которые нет ссылок, затем удалить немаркированные объекты и, наконец, отсортировать и сжать эти несохраненные объекты для завершения сборки мусора.
Для контроляGCСтоит и делает выполнение более стабильным, V8 использует инкрементную маркировку, вместо обхода всей кучи пытается пометить каждый возможный объект, обходит только часть кучи, а затем возобновляет нормальное выполнение кода. Следующий GC продолжится с того места, где остановился предыдущий обход. Это позволяет делать очень короткие паузы во время обычного выполнения. Как упоминалось ранее, этап сканирования обрабатывается отдельным потоком.
оптимизированный запасной вариант
Чтобы еще больше повысить эффективность выполнения кода JavaScript в V8, компилятор напрямую генерирует более эффективный машинный код. Когда программа запущена, V8 будет собирать данные о выполнении кода JavaScript. Когда V8 обнаруживает, что функция выполняется часто (механизм встроенных функций), он помечает ее как активную функцию. Для функций горячих точек стратегия V8 более оптимистична, и она склонна думать, что эта функция относительно стабильна и ее тип определен, поэтому компилятор генерирует более эффективный машинный код. В последующей операции, в случае изменения типа, V8 использует функцию JavaScript, чтобы скомпилировать ее в машинный байт-код перед оптимизацией. Например, следующий код:
function add(a, b) {
return a + b;
}
for (var i = 0; i < 10000; ++i) {
add(i, i);
}
add("a", "b"); //千万别这么做!
Рассмотрим следующий пример:
// 片段 1
var person = {
add: function(a, b) {
return a + b;
}
};
obj.name = "li";
// 片段 2
var person = {
add: function(a, b) {
return a + b;
},
name: "li"
};
Функции, реализованные приведенным выше кодом, одинаковы, все они определяют объект, и этот объект имеет атрибутname
и методadd()
. Но использование Фрагмента 2 более эффективно. Фрагмент 1 добавляет свойство к объекту objname
, что приводит к скрытому производному классу. ** Динамическое добавление и удаление свойств объекта приводит к созданию нового скрытого класса. **Если функция добавления объекта была оптимизирована для создания более эффективного кода, измененный объект не может использовать оптимизированный код из-за добавления или удаления свойств.
Из примера мы видим, что
Чем конкретнее типы параметров внутри функции, тем лучше V8 может генерировать оптимизированный код.
заключительные замечания
Что ж, содержание этой статьи, наконец, закончилось.Сказав так много, вы действительно понимаете, как мы можем написать более оптимизированный код, чтобы удовлетворить хобби компилятора?
порядок свойств объекта: свойства объекта всегда создаются в одном и том же порядке, чтобы можно было совместно использовать скрытые классы и впоследствии оптимизированный код.
динамические свойства: добавление свойств к объекту после создания экземпляра заставит скрытый класс измениться и замедлит любые методы, оптимизированные для ранее скрытых классов, поэтому вместо этого используйте назначение всех свойств объекта в конструкторе.
метод: код, многократно выполняющий один и тот же метод, будет работать быстрее, чем код, выполняющий его только один раз (из-за встроенного кэширования).
множество: избегайте разреженных массивов, ключи которых не являются инкрементными числами. Разреженный массив — это хеш-таблица. Доступ к элементам в таких массивах требует больших затрат. Кроме того, старайтесь избегать предварительного выделения больших массивов, предпочтительно по требованию, с автоматическим увеличением. Наконец, не t удалять элементы в массиве, это делает ключи разреженными.