JavaScript v8

v8 советы для JavaScript

Дэниел Клиффорд сделал на Google I/O прекрасный доклад, посвященный особенностям оптимизации кода JavaSсript для движка V8. Дэниел призвал нас стремиться к большей скорости, тщательно анализировать отличия между С++ и JavaScript, и писать код, помня о том, как работает интерпретатор. Я собрал в этой статье резюме самых главных моментов выступления Дэниела, и буду обновлять её по мере того, как движок будет меняться.

Самый главный совет

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

Лучшая стратегия, ведущая к созданию быстрого веб-приложения выглядит так:

  • Продумайте всё заранее, до того как столкнётесь с проблемами.
  • Тщательно разберитесь и проникните в суть проблемы.
  • Исправляйте только то, что имеет значение.

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

Итак, приступим.

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

На этапе компиляции информация о типах в JavaScript очень ограничена: во время исполнения типы могут меняться, так что вполне естественно ожидать, что при компиляции трудно делать предположения о них. Возникает вопрос — как в таких условиях можно хотя бы приблизиться к скорости С++? Тем не менее, V8 ухитряется создавать скрытые классы для объектов во время выполнения. Объекты, имеющие один и тот же класс, разделяют один и тот же оптимизированный код.

Например:



function Point(x, y) {
  this.x = x;
  this.y = y;
}

var p1 = new Point(11, 22);
var p2 = new Point(33, 44);
// At this point, p1 and p2 have a shared hidden class
p2.z = 55;
// warning! p1 and p2 now have different hidden classes!


Пока к p2 не было добавлено свойство « .z «, p1 и p2 внутри компилятора имели один и тот же скрытый класс, и V8 мог использовать один и тот же оптимизированный машинный код для обоих объектов. Чем реже вы будете менять скрытый класс, тем лучше будет производительность.

Выводы:
  • Инициализируйте все объекты в конструкторах, чтобы они как можно меньше менялись в дальнейшем.
  • Всегда инициализируйте свойства объекта в одном и том же порядке.

Числа

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

Например:



var i = 42;  // this is a 31-bit signed integer
var j = 4.2;  // this is a double-precision floating point number


Выводы:
  • Старайтесь использовать 31-битные целые со знаком везде, где это возможно.

Массивы

V8 использует два вида внутреннего представления массивов:

  • Настоящие массивы для компактных последовательных наборов ключей.
  • Хэш-таблицы в остальных случаях.
Выводы:
  • Не стоит заставлять массивы перепрыгивать из одной категории в другую.
  • Используйте непрерывную нумерацию индексов, начиная с 0 (в точности как в С).
  • Не заполняйте предварительно большие массивы (содержащие больше 64K элементов) — это ничего не даст.
  • Не удаляйте элементы из массивов (особенно числовых).
  • Не обращайтесь к неинициализированным или удалённым элементам. Пример:


a = new Array();
  for (var b = 0; b < 10; b++) {
  a[0] |= b;  // Oh no!
  }
  //vs.
  a = new Array();
  a[0] = 0;
  for (var b = 0; b < 10; b++) {
  a[0] |= b;  // Much better! 2x faster.
  }


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



var a = new Array();
  a[0] = 77;   // Allocates
  a[1] = 88;
  a[2] = 0.5;   // Allocates, converts
  a[3] = true; // Allocates, converts


Гораздо быстрее будет так:



var a = [77, 88, 0.5, true];


В первом примере индивидуальные присваивания происходят последовательно, и в тот момент, когда a[2] получает значение, компилятор преобразует a в массив распакованных чисел с двойной точностью, а когда a[3] инициализируется нечисловым элементом, происходит обратное преобразование. Во втором примере компилятор сразу выберет нужный тип массива.
Таким образом:

  • Маленькие фиксированные массивы лучше всего инициализировать, используя литерал массива.
  • Заполняйте маленькие массивы (<64К) перед использованием.
  • Не храните нечисловые значения в числовых массивах.
  • Старайтесь избегать преобразований при инициализации не через литералы.

Компиляция JavaScript

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

  • Базовый компилятор, который генерирует код для всего скрипта.
  • Оптимизирующий компилятор, который генерирует очень быстрый код для самых «горячих» участков. Такая компиляция занимает больше времени.

Базовый компилятор

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

Выводы:
  • Предпочитайте мономорфные операторы полиморфным.

Оператор является мономорфным, если скрытый тип операндов всегда одинаков, и полиморфным, если он может меняться. Например, второй вызов add() делает код полиморфным:



function add(x, y) {
  return x + y;
  }

  add(1, 2);      // + in add is monomorphic
  add("a", "b");  // + in add becomes polymorphic


Оптимизирующий компилятор

Параллельно с работой базового компилятора, оптимизирующий компилятор перекомпилирует «горячие», то есть такие, которые выполняются часто, участки кода. Он использует информацию о типах, накопленную в инлайн-кэшах.

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

Вы можете посмотреть, что именно оптимизируется в вашем коде, используя автономную версию движка d8:



d8 --trace-opt primes.js


(имена оптимизированных функций будут выведены в stdout )

Не все функции могут быть оптимизированы. В частности, оптимизирующий компилятор пропускает любые функции, содержащие блоки try/catch .

Выводы:

Если необходимо использовать блок try/catch , помещайте критичный к производительности код снаружи. Пример:

Вывод:

  • Положить перфорация-код в чувствительные вложенные функции если у вас есть try {} catch {} блоки:


function perf_sensitive() {
  // Do performance-sensitive work here
}

try {
  perf_sensitive()
} catch (e) {
  // Handle exceptions here
}


Возможно, в будущем ситуация изменится, и мы сможем компилировать блоки try/catch оптимизирующим компилятором. Вы можете посмотреть, какие именно функции игнорируются, указав опцию --trace-bailout при запуске d8:



d8 --trace-bailout primes.js


Деоптимизация

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

Выводы:
  • Избегайте изменения скрытых классов в оптимизированных функциях.

Вы можете посмотреть, какие именно функции подвергаются деоптимизации, запустив d8 с опцией --trace-deopt :



d8 --trace-deopt primes.js


Другие инструменты V8

Перечисленные выше функции могут быть переданы Google Chrome при запуске:



"/Applications/Google Chrome.app/Contents/MacOS/Google 
Chrome" --js-flags="--trace-opt --trace-deopt --trace-bailout"


В d8 тоже есть профилировщик:



% out/ia32.release/d8 primes.js --prof


Сэмплирующий профилировщик d8 делает снимки каждую миллисекунду и пишет в v8.log .

Заключение

Важно понимать, как устроен движок V8, чтобы писать хорошо оптимизированный код. И не забывайте об общих принципах, описанных в начале статьи:

  • Продумайте всё заранее, до того как столкнётесь с проблемами.
  • Тщательно разберитесь и проникните в суть проблемы.
  • Исправляйте только то, что имеет значение.

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

Статья подготовлена для вас коллективом сайта www.pixelcom.crimea.ua
Оригинал статьи: www.html5rocks.com/v8

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

Ваш e-mail не будет опубликован. Обязательные поля помечены *


5 + 7 =

Можно использовать следующие HTML -теги и атрибуты: <a href= http://pixelcom.crimea.ua/"" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>