Советы по оптимизации V8 и JavaScript

V8 – это открытый движок для компиляции JavaScript, разработанный в Google. У Firefox для этого есть собственный движок под названием SpiderMonkey, он очень похож на V8, хотя некоторые отличия все же есть. В этом руководстве мы сосредоточимся на движке V8.

Вот пара основных фактов о V8:

Краткий обзор JavaScript

Что же именно происходит, когда мы отправляем наш код JavaScript на обработку движку V8 (после минификации и других вещей, которые мы делаем с кодом)?

Посмотрите на следующую диаграмму, где показаны все шаги:

КОД JavaScript —→ ОБРАБОТКА —→ АБСТРАКТНОЕ СИНТАКСИЧЕСКОЕ ДЕРЕВО

                                             ↓

           ОПТИМИЗИРУЮЩИЙ КОМПИЛЯТОР ←— ИНТЕРПРЕТАТОР

                         ↓                   ↓

                        МАШИННЫЙ КОД —→  БАЙТКОД

Далее мы подробно обсудим каждый шаг.

Кроме того, мы обсудим, как анализируется код и как передать побольше кода JavaScript в оптимизирующий компилятор. Оптимизирующий компилятор (он же Turbofan) берет код JavaScript и преобразует его в высокопроизводительный машинный код, поэтому чем больше кода мы ему дадим, тем быстрее будет приложение. К слову, интерпретатор в Chrome называется Ignition.

Анализ кода JavaScript

Итак, первая обработка кода JavaScript – это его синтаксический анализ, или парсинг. Давайте подробно обсудим, что это такое.

Парсинг бывает двух видов:

  • Полный синтаксический анализ (eager parse) – сразу анализирует каждую строку кода
  • Ленивый, или предварительный анализ (lazy parse) – анализирует необходимый минимум, а остальное откладывает на потом.

Какой лучше? Это зависит от вашей ситуации.

Давайте посмотрим на такой код.

// полный анализ деклараций

const a = 1;

const b = 2;

// ленивый анализ, можно оставить на потом

function add(a, b) {

  return a + b;

}

// ой, кажется, add все же нужна сейчас, возвращаемся назад

add(a, b);

Итак, объявления переменных анализируются полностью, а функция – лениво. Это замечательно, пока мы не встречаем add(a, b) ниже: функция add нужна нам сразу, поэтому было бы лучше обработать ее так же, как и переменные.

Чтобы сразу же проанализировать функцию add, мы можем сделать так:

// полный анализ деклараций

const a = 1;

const b = 2;

// полный анализ функции

var add = (function(a, b) {

  return a + b;

})();

// это уже можно использовать, потому что мы проанализировали функцию

add(a, b);

Так создается большинство используемых вами модулей.

Встраивание функций

Иногда Chrome существенно переписывает ваш JavaScript, например, использует встраивание функции.

В качестве примера посмотрим на следующий код:

const square = (x) => { return x * x }

const callFunction100Times = (func) => {

  for(let i = 0; i < 100; i++) {

    // the func param will be called 100 times

    func(2)

  }

}

callFunction100Times(square)

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

const square = (x) => { return x * x }

const callFunction100Times = (func) => {

  for(let i = 100; i < 100; i++) {

    // the function is inlined so we don't have

    // to keep calling func

    return x * x

  }

}

callFunction100Times(square)

Как видим, V8 по сути удаляет тот шаг, на котором мы вызываем функцию func, и вместо этого вставляет тело square. Это очень удобно, так как улучшает производительность кода.

Ошибка встраивания функции

Однако в этом подходе есть небольшая ошибка. Давайте рассмотрим следующий пример кода:

const square = (x) => { return x * x }

const cube = (x) => { return x * x * x }

const callFunction100Times = (func) => {

  for(let i = 100; i < 100; i++) {

    // the function is inlined so we don't have

    // to keep calling func

    func(2)

  }

}

callFunction100Times(square)

callFunction100Times(cube)

Итак, на этот раз после того, как мы вызвали функцию square 100 раз, мы вызываем функцию cube  – тоже 100 раз. Прежде чем cube можно будет вызвать, мы должны деоптимизировать callFunction100Times, поскольку функция square встроена. В таких случаях функция square будет казаться быстрее, чем cube. На самом же деле деоптимизация увеличивает время ее выполнения.

Объекты

Что касается объектов, V8 имеет целую встроенную систему типов, которая умеет различать объекты.

Мономорфизм

Это значит, что у объектов одинаковые ключи, без отличий. К примеру:

const person = { name: 'John' }

const person2 = { name: 'Paul' }

Полиморфизм

У объектов общая структура, но имеются небольшие отличия.

const person = { name: 'John' }

const person2 = { name: 'Paul', age: 27 }

Мегаморфизм

Тут объекты совершенно разные, и их нельзя сравнивать.

const person = { name: 'John' }

const building = { rooms: ['cafe', 'meeting room A', 'meeting room B'], doors: 27 }

Итак, теперь вы знаете, какие объекты бывают в V8. Давайте теперь посмотрим, как V8 оптимизирует объекты.

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

Через скрытые классы V8 идентифицирует объекты. Давайте рассмотрим это по частям.

Итак, мы объявляем объект:

const obj = { name: 'John'}

Затем V8 объявляет classId для этого объекта.

const objClassId = ['name', 1]

После чего создается объект, это делается следующим образом:

const obj = {...objClassId, 'John'}

Потом, когда мы обращаемся к свойству name объекта:

obj.name

V8 выполняет следующий поиск:

obj[getProp(obj[0], name)]

Это процесс, через который проходит V8 при создании объектов. А теперь давайте посмотрим, как мы можем оптимизировать объекты и повторно использовать classId.

Советы по созданию объектов

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

class Point {

  constructor(x,y) {

    this.x = x

    this.y = y

  }

}

const p1 = new Point(11, 22) // hidden classId created

const p2 = new Point(33, 44)

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

const obj = { a: 1 } // создан скрытый класс

obj.b = 3

const obj2 = { b: 3 } // создан еще один скрытый класс

obj2.a = 1

// вот как лучше делать

const obj = { a: 1 } // скрытый класс создан

obj.b = 3

const obj2 = { a: 1 } // и повторно использован

obj2.b = 3

Общие советы по оптимизации

А теперь давайте поговорим о некоторых общих вещах, которые помогут лучше оптимизировать код JavaScript.

Исправление типов аргументов

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

Возьмем следующий пример:

function add(x,y) {

  return x + y

}

add(1,2) // мономорфный

add('a', 'b') // полиморфный

add(true, false)

add({},{})

add([],[]) // мегаморфный - на данном этапе уже было 4+ попытки, оптимизации не будет

Еще один совет – обязательно объявляйте классы в глобальной области:

// не надо так делать

function createPoint(x, y) {

  class Point {

    constructor(x,y) {

      this.x = x

      this.y = y

    }

  }

  // каждый раз создается новый точечный объект

  return new Point(x,y)

}

function length(point) {

  //...

}

Заключение

Надеемся, вы узнали кое-что о том, как устроены внутренности движка V8 и как писать оптимизированный код JavaScript.

Читайте также: JavaScript ES2020: что нового?

Tags: ,

Добавить комментарий