Как работает JavaScript: 5 советов по оптимизации кода в движке V8

внешний интерфейс JavaScript

Как работает JavaScript: 5 советов по оптимизации кода в движке V8

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

  Первая статьяОсновное внимание уделяется обзору механизма, среды выполнения и стека вызовов. Во второй статье мы углубимся во внутренности движка Google JavaScript V8. Мы также даем несколько быстрых советов о том, как лучше писать код JavaScript.SessionStackЛучшие практики, которым следуют команды разработчиков при разработке продуктов.

Обзор

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

   Вот несколько популярных проектов, реализующих движки JavaScript:

  • V8 —  Разработанный Google движок с открытым исходным кодом, написанный на C++.
  • Rhino —  Управляется Mozilla Foundation, движком с открытым исходным кодом, полностью разработанным на Java.
  • SpiderMonkey —  Первый движок JavaScript, который в то время поддерживал Netscape Navigator, а теперь является движком Firefox.
  • JavaScriptCore —  Движок с открытым исходным кодом, разработанный Apple для браузера Safari и продвигаемый под названием Nitro.
  • KJS —  Движок KDE, первоначально разработанный Харри Портеном для веб-браузера Konqueror проекта KDE.
  • Chakra (JScript9) —  Двигатель ИЭ
  • Chakra (JavaScript) —  Движок Microsoft Edge
  • Nashorn- Движок с открытым исходным кодом, разработанный Oracle Java Language Tools Group, часть OpenJDK.
  • JerryScript —  Это легкий движок для IoT

Зачем создавать двигатель V8?

  Двигатель V8 используется GoogleC++Разработан движок с открытым исходным кодом, этот движок также используется в Google Chrome. В отличие от других движков, движок V8 также используется для запуска Node.js.

  V8 изначально был разработан для повышения производительности выполнения JavaScript внутри браузера. Для повышения скорости V8 компилирует код JavaScript в более эффективный машинный код вместо использования интерпретатора. Как и многие современные движки JavaScript, такие как SpiderMonkey или Rhino (Mozilla), он компилирует код JavaScript в машинный код с помощью компилятора «точно в срок». Основное отличие состоит в том, что V8 не генерирует байт-код или какой-либо промежуточный код.

Раньше у V8 было два компилятора

   До выпуска v5.9 V8 (выпущенного ранее в этом году) было два компилятора:

  • full-codegen —  Простой и очень быстрый компилятор, генерирующий простой, но относительно медленный машинный код.
  • Crankshaft —  Более сложный (точно в срок) оптимизирующий компилятор, создающий высокооптимизированный код.

Двигатель   V8 также использует несколько потоков:

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

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

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

Потом,CrankshaftНачать оптимизацию в другом потоке. Он преобразует абстрактное синтаксическое дерево JavaScript вHydrogen, и попытайтесь оптимизировать этот график Hydrogen. Большинство оптимизаций выполняется на этом уровне.

Встраивание кода (встраивание)

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

Скрытый класс

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

   Большинство интерпретаторов JavaScript используют словарную структуру (на основехэш-функция) для сохранения местоположения в памяти значения свойства объекта. Эта структура делает получение значения свойства в JavaScript гораздо более ресурсоемким, чем в нединамических языках, таких как Java или C#. В Java все значения свойств определяются в фиксированном макете объекта перед компиляцией и не могут быть динамически добавлены или удалены во время выполнения (конечно, в C# также естьдинамический тип, но это уже другая тема). Следовательно, значения свойств (или указатели на эти свойства) могут храниться в памяти в виде непрерывного буфера с фиксированным смещением между каждым значением. Длину смещения можно легко определить на основе типа свойства, что невозможно в JavaScript, поскольку тип свойства может измениться во время выполнения.

   Поскольку очень неэффективно использовать словарь для определения местоположения атрибутов объекта в памяти, V8 использует другой метод:скрытый класс. Скрытые классы работают так же, как макеты фиксированных объектов (классы), используемые в таких языках, как Java, за исключением того, что они создаются во время выполнения. Теперь давайте посмотрим, как они выглядят на самом деле:

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» начнет выполняться (в функции «Point»), V8 создаст второй скрытый класс с именем «C1» на основе «C0». «C1» описывает расположение (относительно указателя объекта) значения свойства x в памяти. В этом примере присутствует "x"значение смещенияГде 0, что означает, что когда точка в памяти, когда объект является непрерывным буфером, что соответствует смещению первого атрибута, равно "x". V8 также используется для обновления переключения класса «C0», если к точке объекта добавляется атрибут «x», за переключением класса с «C0» на «C1». Что ж, теперь за классом этой точки находится объект «C1».

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

   Этот процесс повторяется, когда выполняется оператор «this.y = y» (опять же, внутри функции Point, после оператора «this.x = x»).

Создается новый скрытый класс "C2", если к объекту Point добавляется свойство "y" (которое уже содержит свойство "x"), к "C1" добавляется тот же процесс, приведение типов, и начинается скрытый класс Обновите до «C2», и скрытый класс объекта Point будет обновлен до «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». Следовательно, p1 и p2 заканчиваются разными путями перехода классов, с разными скрытыми классами. На самом деле, как видно из этих двух примеров, лучший способ — инициализировать динамические свойства в том же порядке, чтобы можно было повторно использовать скрытые классы.

Встроенное кэширование

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

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

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

   Так как же связаны концепции скрытых классов и встроенного кэширования? Всякий раз, когда вызывается метод для определенного объекта, механизм V8 ищет скрытый класс объекта, чтобы определить значение смещения для получения определенного свойства. Когда один и тот же метод вызывается дважды для одного и того же скрытого класса, V8 пропускает поиск скрытого класса и добавляет значение смещения этого свойства к указателю самого объекта. Для всех будущих вызовов этого метода механизм V8 будет предполагать, что скрытый класс не изменился, и будет переходить непосредственно к местоположению в памяти определенного свойства, что выполняется через значение смещения, сохраненное во время предыдущего поиска. Это значительно повышает скорость выполнения V8.

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

   Эти два объекта в основном одинаковы, но свойства «a» и «b» создаются в другом порядке.

скомпилировать в машинный код

   После того, как график водорода оптимизирован, Crankshaft уменьшает график до представления более низкого уровня, называемого литием. Большинство реализаций Lithium ориентированы на определенную структуру. Распределение регистров происходит на этом уровне.

   Наконец, Lithium компилируется в машинный код. Затем начинается OSR: метод замены в стеке, который заменяет текущие кадры стека во время выполнения. Мы можем запустить его, когда начнем компилировать и оптимизировать явно трудоемкий метод. V8 не отбрасывает медленный код, который работал раньше, а затем переходит к оптимизированному коду. Вместо этого V8 преобразует контекст этого кода (стек, регистры), чтобы переключиться на оптимизированную версию на пути к выполнению этого медленного кода. Это очень сложная задача, учитывая, что V8 уже включает код среди других оптимизаций. Конечно, V8 — не единственный двигатель, способный на это.

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

вывоз мусора

   Для сборки мусора V8 использует традиционный метод пометки и очистки поколений для очистки данных старого поколения. Фаза разметки предотвращает запуск JavaScript. Чтобы контролировать стоимость сборки мусора и сделать выполнение JavaScript более стабильным, V8 использует инкрементную маркировку: вместо обхода всей кучи для маркировки всех возможных объектов он проходит только часть кучи, а затем возобновляет нормальное выполнение. Следующая сборка мусора начинается там, где остановился предыдущий обход, что делает паузы между каждым обычным выполнением очень короткими. Как упоминалось ранее, операции очистки выполняются отдельными потоками.

Зажигание и турбовентилятор

   С выпуском V8 5.9 ранее в 2017 году был представлен новый конвейер выполнения. Этот новый конвейер выполнениядействительныйВ приложениях JavaScript были достигнуты более значительные улучшения производительности и значительная экономия памяти.

   Этот новый конвейер выполнения построен на интерпретаторе V8.Ignitionи последний оптимизирующий компиляторTurboFanнад.

Ты сможешьздесьОзнакомьтесь со всеми сообщениями в блоге команды V8 по этой теме.

Начиная с версии 5.9 V8, команда V8 усердно работала, чтобы не отставать от функций языка JavaScript и оптимизации для этих функций, в то время как full codegen и Crankshaft (обе технологии, которые обслуживают V8 с 2010 года) не используются. JavaScript.

   Это будет означать, что весь V8 будет иметь более простую и удобную в сопровождении архитектуру.

   Улучшения в Интернете и Node.js

   Конечно, эти улучшения — только начало. Новые конвейеры Ignition и TurboFan открывают путь для дальнейшей оптимизации, которая улучшит производительность JavaScript и сделает V8 еще более эффективным с точки зрения ресурсов в Chrome и Node.js на долгие годы.

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

Как написать оптимизированный JavaScript

  1. порядок свойств объекта: Обязательно используйте тот же порядок при создании экземпляров свойств объекта, чтобы скрытые классы и последующий код оптимизации могли совместно использоваться.
  2. динамические свойства: добавление свойств после того, как объект был создан, вызывает изменение скрытого класса и замедляет выполнение кода, оптимизированного для старого скрытого класса. Итак, назначьте все свойства в конструкторе объекта.
  3. метод: повторное выполнение одного и того же метода будет выполняться быстрее, чем однократное выполнение разных методов (из-за встроенного кэширования).
  4. множество: Избегайте использования разреженных массивов, чьи ключи не являются увеличивающими числами.хеш-таблица. Извлечение каждого элемента в таком массиве требует больших затрат. В то же время избегайте подачи заявки на большие массивы заранее. Лучшей практикой является медленное увеличение массива по мере необходимости. Наконец, не удаляйте элементы в массиве, так как это сделает ключи разреженными.
  5. Помеченные значения: V8 использует 32 бита для представления объектов и чисел. Он использует один бит, чтобы различать, является ли это объектом (флаг = 1) или целым числом (флаг = 0), также называемым малым целым числом (SMI), потому что оно имеет только 31 бит. Затем, если число больше 31 бита, V8 упакует его, преобразует в двойное и создаст новый объект для хранения числа. Итак, чтобы избежать дорогостоящих операций с ящиками, попробуйте использовать 31-битные числа со знаком.

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

больше ресурсов


Программа перевода самородковэто сообщество, которое переводит высококачественные технические статьи из Интернета сНаггетсДелитесь статьями на английском языке на . Охват контентаAndroid,iOS,React,внешний интерфейс,задняя часть,товар,дизайнЕсли вы хотите видеть более качественные переводы, пожалуйста, продолжайте обращать вниманиеПрограмма перевода самородков,официальный Вейбо,Знай колонку.