Википедия, Свеча зажигания:

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

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

Пусть история начнется

Когда я освежал свои знания о том, как работают компиляторы JavaScript V8, я натолкнулся на кое-что интересное. Это был новый неоптимизирующий компилятор, который позволяет двигателю V8 запускаться быстро и увеличивает его реальную производительность на 5–15%. Эта история, которую я вам расскажу, будет о сочетании современных и старых компиляторов. Кроме того, я проведу исследование компилятора среднего уровня и объясню, почему Full-Codegen (старый неоптимизирующий базовый компилятор) мертв.

Что у нас было раньше: история компиляторов JavaScript V8

Давным-давно конвейер компилятора JavaScript V8 имел совершенно иную структуру. Он состоял из двух разных компиляторов, и они сделали свою работу:

  • Full-codegen - очень быстрый компилятор, производящий грязный и медленный машинный код;
  • Коленчатый вал - оптимизирующий компилятор JIT.

Изменились стандарты, вырос технический долг, и мобильные телефоны оказались более популярными, чем настольные компьютеры. В конце концов, Chrome V8 потреблял слишком много оперативной памяти и не смог оптимизировать как базовые старые вещи JavaScript (например, try, catch и, наконец, блоки), так и новые вещи из ES2015.

С выпуском версии V8 5.9 у нас появился новый конвейер выполнения. Идея возникла из парадигмы прежде всего мобильные »с акцентом на сокращение потребления памяти V8, что означает не только визуальный дизайн, но и архитектуру программного обеспечения, подходящую для мобильных телефонов. Речь шла об оптимизации использования памяти и ЦП на устройствах с небольшим объемом памяти и недостаточным энергопотреблением, таких как Android. Кроме того, в старом конвейере были некоторые пробелы, такие как плохая (многоуровневая, устаревшая) архитектура, которую необходимо было заполнить.

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

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

Технопедия , « Фрейм стека »:

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

Кадр стека, также известный как кадр активации или запись активации.

Новый конвейер исполнения включал следующее:

  • Ignition - быстрый низкоуровневый интерпретатор на основе регистров;
  • TurboFan - новый оптимизирующий компилятор V8.

Хм ... Чего-то не хватает. Где старый добрый неоптимизированный код?

Команда V8 экспериментировала с комбинацией Ignition и TurboFan, Full-codegen и Crankshaft.

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

Тогда это зависело от того, какие функциональные возможности имеет функция: это будет относиться либо к Crankshaft, либо к TurboFan, а TurboFan должен был поддерживать как компиляцию из байт-кода Ignition, так и из Full-Codegen / AST. У этого было много дополнительных сложностей в поддержании согласованности между Ignition и FCG, например маркировка точек приостановки async / await или потенциальных мест деоптимизации.

Ужасно звучит!

Эта комбинация получила название Frankenpipeline и даже стала частью производства версии V8. Здесь вы можете получить дополнительную информацию о пайплайне 2016 года с компиляцией базовой линии.

Росс Макилрой. « Запуск интерпретатора зажигания »:

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

Файл документа зажигания:

Сам интерпретатор состоит из набора фрагментов кода обработчика байт-кода, каждый из которых обрабатывает определенный байт-код и отправляет обработчику следующий байт-код. Эти обработчики байт-кода написаны в высокоуровневой форме ассемблерного кода, не зависящей от машинной архитектуры, как реализовано классом CodeStubAssembler и скомпилировано Turbofan (или Sparkplug, но мы расскажу об этом чуть позже).

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

TurboFan имеет многоуровневую архитектуру. Он гораздо более общий и имеет меньшую кодовую базу с уровнями абстракции и без спагетти-кода, позволяет легко улучшать (для новых стандартов ES), и проще добавить туда генераторы кода для конкретной машины (например, код для IBM, ARM, Intel архитектуры). Основная идея TurboFan - промежуточное представление моря узлов (IR). TurboFan принимает инструкции от Ignition, оптимизирует их и создает машинный код для конкретной платформы.

У Crankshaft был более специализированный IR, специфичный для JS, который оставался довольно высокоуровневым, и он хранил CFG (граф потока управления) с явным упорядочением операций, что сделало его быстрым для компиляции, но также затруднило добавление новых функций.

Turbofan имеет больше уровней IR для перехода от операций JS через абстрактные операции к машинным операциям, а Sea-of-Nodes позволяет свободное перепланирование без такой тесной связи с потоком управления, но эта универсальность достигается за счет затрат времени выполнения.

Узнайте больше об идее из статьи Запуск Ignition и TurboFan

Также ознакомьтесь с статьей интерпретатора зажигания и историей о зажигании V8 и TurboFan.

Всегда ли нам нужен оптимизированный результат?

Оптимизация - это процесс, требующий времени и вычислительных мощностей. Таким образом, не стоит тратить усилия на короткие сеансы JavaScript, такие как загрузка веб-сайтов или использование некоторых инструментов командной строки. Поскольку декодирование байт-кода в TurboFan слишком дорого и требует много времени для таких случаев, вы можете повысить производительность и ускорить запуск приложения, только удалив шаг оптимизации (или упростив его).

Идея компилятора среднего уровня V8 пришла к группе разработчиков V8 Core несколько лет назад (~ 2016 г.). Они пытались реализовать прототипы на V8 Mobile London Hackathon, когда команда экспериментировала с включением его в процесс компиляции между Ignition и TurboFan.

Они разработали четыре возможных варианта реализации:

  • SparkPlug - однопроходный неоптимизирующий базовый компилятор;
  • SparkPlugOpt - однопроходный компилятор базовых показателей со спекулятивной оптимизацией определенных мономорфных операций;
  • TurboFan-Lite - TurboFan с отключенным встраиванием;
  • TurboProp - оптимизирующий компилятор, который использует урезанный вариант конвейера TurboFan с попыткой приблизительно оценить влияние другого бэкэнда на TurboFan.

Вы можете узнать больше об идее Mid-Tier Compiler из аналитического документа.

И после некоторого расследования команда V8 решила внедрить TurboProp. TurboProp - это более быстрая и легкая версия TurboFan, в которой отключены некоторые серьезные оптимизации. Это оптимизатор среднего уровня. Но они решили пойти другим путем, потому что в некоторых случаях оптимизация вообще не нужна. Решение прямо здесь, в исходном коде V8, и есть флаг для его включения:

DEFINE_BOOL(turboprop, false, "enable experimental turboprop mid-tier compiler")

Или даже можно полностью заменить TurboFan на TurboProp:

DEFINE_BOOL(
    turboprop_as_toptier, false,
    "enable experimental turboprop compiler without further tierup to turbofan")

Дополнительные флаги V8 можно найти здесь.

Кажется, что Ignition и TurboFan находятся на разных сторонах спектра времени и качества компиляции. По результатам тестирования TurboProp был немного ближе к полнофункциональному оптимизатору TurboFan.

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

Да, деоптимизация происходит, например, когда вы меняете переменную, которая долгое время была постоянной в вашем коде, или используете 32/64-битные двойные вместо 31/32-битных целых чисел, называемые Smi (в зависимости от 32 / 64- битовая платформа). Ознакомьтесь с интересной глубоководной статьей.

Википедия. Горячая точка (компьютерное программирование) »:

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

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

Сделали ли они шаг назад к Full-codegen? - Нет (и да, немного)

В любом случае, теперь пришло время познакомиться с новым компилятором базовой линии - Sparkplug!

Старый-новый шаг - Sparkplug, неоптимизирующий компилятор

Sparkplug - это транспилятор / компилятор, который преобразует байт-код Ignition в машинный код, переводя JS-код из режима виртуальной машины / эмулятора в исходный.

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

Встроенные в V8 обозначают фрагменты кода, которые выполняются машиной / виртуальной машиной во время выполнения. Обычно вы можете реализовать функции встроенных объектов (таких как RegExp или Promise), но встроенные функции также могут использоваться для обеспечения различных внутренних функций. Узнайте больше из Документа о встроенных функциях CodeStubAssembler.

Sparkplug компилируется из байт-кода (созданного Ignition, как мы выяснили ранее) путем повторения байт-кода и выдачи машинного кода для каждого байт-кода как посещенного / отмеченного.

Поток управления: простые операции (сравнение ссылок или typeof) могут выполняться напрямую. Более сложные операции (например, арифметические) перенаправляются на встроенные функции. Это делает Sparkplug ОЧЕНЬ быстрым компилятором. Большая часть работы уже сделана Ignition, поэтому Sparkplug может пройти через сгенерированный байт-код и выполнить свою работу.

Sparkplug не генерирует никакого промежуточного представления (IR), как это делают большинство компиляторов. Вместо этого он компилируется непосредственно в машинный код за один линейный проход по байт-коду, генерируя код, который соответствует выполнению этого байт-кода. Весь компилятор представляет собой инструкцию switch внутри цикла for, отправляющуюся в фиксированные функции генерации машинного кода для каждого байт-кода.

« Sparkplug - неоптимизирующий компилятор JavaScript »:

Отсутствие IR (промежуточного представления) означает, что компилятор имеет ограниченные возможности оптимизации, помимо очень локальной оптимизации с глазком. Это также означает, что мы должны переносить всю реализацию отдельно на каждую поддерживаемую нами архитектуру, поскольку не существует промежуточного этапа, независимого от архитектуры. Но оказалось, что ни то, ни другое не является проблемой: быстрый компилятор - это простой компилятор, поэтому код довольно легко переносить; и Sparkplug не нуждается в серьезной оптимизации, так как у нас в любом случае есть отличный оптимизирующий компилятор.

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

Кроме того, Sparkplug поддерживает структуру стека интерпретатора, поэтому замена в стеке остается простой.

Что касается фреймов стека Sparkplug, они почти 1: 1 совместимы с фреймами стека Ignition. Узнайте больше о том, как Sparkplug использует фреймы стека, в статье о Sparkplug.

Представление

Свечи зажигания очень-очень быстро справляются со своей задачей. Это в 10–1000 раз быстрее, чем TurboFan / TurboProp. Это происходит сразу после компиляции байт-кода (фаза зажигания). И проверено как на Octane, так и на реальных примерах (facebook.com test). Кроме того, машинный код Sparkplug в 5–6 раз больше, чем байт-код Ignition. Вот подробнее о производительности и подробнее о тестах.

Разве в V8 раньше не использовался быстрый компилятор, который является Full-codegen? - Да, и его убрали в пользу Ignition.

Большая разница между Full-codegen и Sparkplug заключается в том, что последний компилируется из байт-кода, а не из исходного кода или AST.

Еще во времена полной генерации кода Crankshaft пришлось повторно анализировать исходный код в AST и компилировать оттуда. Или, что еще хуже, чтобы иметь возможность деоптимизировать код обратно в Full-codegen, ему нужно было как бы воспроизвести компиляцию Full-codegen, чтобы получить правильный кадр деоптимизированного стека.

Да, Turbofan теперь деоптимизируется до Sparkplug, а не до Ignition.

В V8 9.3 они представили пакетную компиляцию. О пакетной компиляции я расскажу ниже.

Свечи зажигания заработали?

Это «да» для V8 9.2.x-9.4.x

#if V8_TARGET_ARCH_IA32 || V8_TARGET_ARCH_X64 || V8_TARGET_ARCH_ARM64 ||     \
    V8_TARGET_ARCH_ARM || V8_TARGET_ARCH_RISCV64 || V8_TARGET_ARCH_MIPS64 || \
    V8_TARGET_ARCH_MIPS
#define ENABLE_SPARKPLUG true

Да, но не для всех архитектур. Тем не менее, большинство настольных архитектур имеют флаг true:

Но он по-прежнему отключен на Android.

#if ENABLE_SPARKPLUG && !defined(ANDROID)
// Enable Sparkplug by default on desktop-only.
#define ENABLE_SPARKPLUG_BY_DEFAULT true
#else
#define ENABLE_SPARKPLUG_BY_DEFAULT false
#endif

И еще код. Включает Sparkplug для особых условий, baseline.cc файл:

Если для платформы включен Sparkplug, он будет использоваться для компиляции кода партиями (пакетами) по 4 КБ. Он был введен в V8 9.3: вместо того, чтобы компилировать каждую функцию по отдельности, он компилирует несколько функций в пакете. Да, по соображениям безопасности V8 защищает от записи память кода, которую он генерирует, требуя переключения разрешений между записываемыми (во время компиляции) и исполняемыми (а это дорогостоящая операция и может быть узким местом).

Это снижает стоимость переключения разрешений на страницу памяти, выполняя это только один раз за пакет. Среднее время компиляции (Ignition + Sparkplug) сокращено до 44%.

Читайте больше по этой теме в блоге, а другие флаги - на GitHub.

Вот интересный фрагмент кода из runtime-internal.cc, он посвящен пакетной компиляции и разметке кода для оптимизации:

Итак, сначала V8 компилирует код с помощью Sparkplug, затем отмечает код для оптимизации, и наступает время для Turbofan!

Резюме

Теперь в V8 есть новый быстрый неоптимизирующийся компилятор среднего уровня. Он включен в настольном Chrome для многих архитектур. Хотя он все еще отключен на Android (V8 9.4.x). Что наиболее важно, наличие компилятора среднего уровня повысило общую реальную производительность на 5–15%.

Возможно, в ближайшем будущем мы увидим еще один компилятор среднего уровня, поскольку у команды есть внутренний документ под названием «The Case For Four Tiers». Это означает, что мы встретим новый этап процесса компиляции под названием TurboProp.

Также я хочу упомянуть SpiderMonkey, движок JavaScript Firefox. Он имеет Baseline Interpreter как промежуточный этап компиляции JS. Я должен сказать, SpiderMonkey действительно хорошо справляется с этой задачей! А подробнее вы можете узнать из статьи.

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

Следите за обновлениями! Со своей стороны, я обещаю держать вас в курсе последних событий, касающихся движка V8 JS.

Напишите мне в Твиттер, если у вас есть какие-либо вопросы или мысли!

Или LinkedIn

Полезные ссылки:

- Документ свечи зажигания

- Исследование компилятора среднего уровня

- ТурбоПроп

- Зажигание под капотом

- Файл документа зажигания: генерация байт-кода (он нужен для понимания Sparkplug)

- ТурбоВентилятор под капотом

- V8: За кадром

- Статья о Sparkplug, блог V8

Особая благодарность

Диане Володченко за все иллюстрации к статье!

Инстаграм

Дриббл

И
Leszek Swirski, соавтору V8, за некоторые подробности, обзор и идеи!