- PVSM.RU - https://www.pvsm.ru -

Оптимизация ES2015 Прокси в V8

Оптимизация ES2015 Прокси в V8 - 1

Это перевод поста из официального блога JS-движка V8. Статья короткая, текста мало, больше похоже на увлекательный рассказ о проблемах, подстерегающих ни о чём не подозревающих сотрудников Google в коде V8. Речь пойдёт об ускорении обработки ES6 Прокси [1] в V8, которое будет доступно в Chrome 62 и Node v9.x, и совсем немного о том, как лучше применять прокси для получения максимальной скорости работы.

Введение

Прокси появились в JavaScript с принятием стандарта ES2015. Они позволяют перехватывать фундаментальные операции объектов и переопределять их поведение. Прокси являются основой таких библиотек, как jsdom [2] или Complink RPC library [3]. В последнее время мы приложили много усилий, чтобы улучшить производительность прокси в V8. Эта статья проливает немного света на общие подходы к улучшению производительности в V8 и для прокси в частности.

Прокси — это "объекты, используемые для переопределения фундаментальных операций (например доступ к свойствам, присваивание, перечисление, вызов функции)" (из MDN). Больше информации может быть найдено в полной спецификации [4]. Например, следующий пример кода добавляет логирование обращения к любому свойству объекта:

const target = {};
const callTracer = new Proxy(target, {
  get: (target, name, receiver) => {
    console.log(`get was called for: ${name}`);
    return target[name];
  }
});

callTracer.property = 'value';
console.log(callTracer.property);
// get was called for: property
// value

Создание прокси

Первое, на что мы обратим внимание — это создание прокси. Первоначальная реализация на С++ повторяла шаги из спецификации EcmaScript, что приводило к минимум 4-ём прыжкам между C++ и JS средами выполнения, это видно на нижележащей схеме. Мы хотели перевести эту реализацию на платформонезависимый CodeStubAssembler [5] (CSA), запускаемый в JS-среде исполнения. Это портирование минимизировало бы число прыжков между языковыми средами исполнения. CEntryStub и JSEntryStub на схеме — это и есть среды исполнения. Точечная линия показывает границы между средами исполнения. На наше счастье, большинство вспомогательных объявлений-заглушек (helper predicates) [6] уже были в CSA, благодаря чему начальная версия [7] получилась лаконичной и читабельной.

Схема ниже показывает поток управления при работе прокси с любым перехватчиком (в этом примере перехват apply, который вызывается в момент использования прокси как функции), нарисована она по следующему коду:

function foo(...) {...}
g = new Proxy({...}, {
  apply: foo
});
g(1, 2);

Оптимизация ES2015 Прокси в V8 - 2

После портирования вызова перехватчика на CSA все вызовы происходят в JS-среде, уменьшая количество "прыжков" между языками с 4-х до нуля.

Это изменения привело к следующему улучшению производительности:

Оптимизация ES2015 Прокси в V8 - 3

Наши измерения производительности JS показывают ускорение от 49% до 74%. Грубо говоря мы измеряли, сколько раз конкретный микробенчмарк может быть запущен за 1000 мс. Для некоторых тестов код запускается несколько раз, чтобы уточнить результат (из-за ограниченной точности таймера). Код всех бенчмарков ниже может быть найден в нашей js-perf-test директории.

Call и construct перехватчики

Следующая часть показывает результаты оптимизации перехватчиков вызова и создания (они же apply [8] и construct [9]).

Оптимизация ES2015 Прокси в V8 - 4

Значительное увеличение производительности при вызове прокси — до 500% быстрее! А ускорение создания прокси не так примечательно, особенно если не определены никакие перехватчики — в этом случае ускорение только 25%. Получили мы эти результаты с помощью запуска следующей команды в d8 shell [10]:

Оптимизация ES2015 Прокси в V8 - 5

Где test.js — это файл со следующим содержимым:

function MyClass() {}
MyClass.prototype = {};
const P = new Proxy(MyClass, {});
function run() {
  return new P();
}
const N = 1e5;
console.time('run');
for (let i = 0; i < N; ++i) {
  run();
}
console.timeEnd('run');

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

Get-перехватчик

Следующая секция о том, насколько мы оптимизировали наиболее используемые операции — чтение и запись свойств через прокси. Оказалось что get-перехватчик [11] более запутан, чем предыдущие примеры, из-за особенностей поведения инлайн-кеша. Подробнее про инлайн-кеши вы можете посмотреть в этом видео [12].

В конечном счёте мы получили рабочий порт на CSA со следующими результатами:

Оптимизация ES2015 Прокси в V8 - 6

После применения изменений мы заметили, что размер apk-файла Chrome для Android вырос на ~160Kb, что больше ожидаемого для маленькой функции в 20 строчек, но, к счастью, мы храним подобную статистику. Оказалось, что функция вызывается дважды из другой функции, которая вызывается 3 раза из третьей, которая вызывается 4 раза. Причиной проблемы был агрессивный инлайн функций. В конце концов мы решили проблему вынесением функции в отдельную заглушку (здесь, видимо, имеются ввиду те же заглушки [6], что выше были названы "предикатами"), что сохранило драгоценные килобайты — окончательная версия увеличивала объём apk-файла всего лишь на ~19Kb.

Has-перехватчик

Следующая часть показывает результаты оптимизации has-перехватчика [13]. Мы думали это будет легко (ожидали переиспользования большей части кода из get-перехватчика), но у has своя атмосфера. Отчасти из-за трудно отлаживаемой проблемы обхода цепочки прототипов при вызове in оператором. Результаты улучшения варьируются от 71% от 428%. И опять выигрыш более заметен, если перехватчики определены при создании.

Оптимизация ES2015 Прокси в V8 - 7

Set-перехватчик

Теперь мы переходим к set-перехватчику [14]. И в этот раз нам надо по-разному работать с именованными [15] и индексированными [16] свойствами (элементами). Эти два типа не часть языка JS, а результат внутренних оптимизаций обработки свойств объектов. Изначальная реализация прокси по-прежнему выходит из среды исполнения (для элементов), что опять приводит к пересечению сред исполнения. Тем не менее мы достигли улучшения от 27% до 438% для случаев, когда перехватчик определён, но ценой замедления на 23%, если не определён. Падение производительности здесь обусловлено дополнительными проверками, чтобы различать индексированные и именованные свойства объекта. Для индексированных свойств пока нет никаких улучшений. Вот график с полными результатами:

Оптимизация ES2015 Прокси в V8 - 8

Результаты при реальном использовании

Полученные в jsdom-proxy-benchmark [17]:

Проект jsdom-proxy-benchmark составляет (в прямом смысле слова составляет: собирает в один html-файл) ECMAScript specification [18] с помощью инструмента Ecmarkup [19]. В версии jsdom@11.2.0 [20] (который лежит в основе Ecmarkup) использует прокси для реализации таких структур как NodeList и HTMLCollection. Мы использовали это как бенчмарк, чтобы измерить прозводительность в более приближенном к реальному миру сценарии, чем наши синтетические микро-бенчмарки. За 100 проходов средние результаты такие:

  • Node v8.4.0 (без оптимизаций прокси): 14277 ± 159 ms
  • Node v9.0.0-v8-canary-20170924 [21] (с всего лишь половиной оптимизированных перехватчиков): 11789 ± 308 ms
  • Разница в результатах порядка 2.4 секунд, что означает улучшение на ~17%
    Оптимизация ES2015 Прокси в V8 - 9
  • Перевод NamedNodeMap на прокси улучшил время обработки на:
    • 1.9 секунды в V8 6.0 (Node v8.4.0)
    • 0.5 секунды в V8 6.3 (Node v9.0.0-v8-canary-20170910)

Оптимизация ES2015 Прокси в V8 - 10

Спасибо за результаты, предоставленные TimothyGu [22].

Полученные в Chai.js [23]:

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

  • Node v8.4.0 (без оптимизаций прокси): 4.2863 ± 0.14 s
  • Node v9.0.0-v8-canary-20170924 (с всего лишь половиной оптимизированных перехватчиков): 3.1809 ± 0.17 s
    Оптимизация ES2015 Прокси в V8 - 11

Используемые для оптимизации подходы:

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

  • сделать тесты на производительность для отдельной маленькой фичи
  • добавить больше тестов, проверяющих соответствие спецификации (или написать их с нуля)
  • изучить оригинальную реализацию на С++
  • перевести фичу на платформонезавимый CodeStubAssembler
  • оптимизировать код дальше через создание TurboFan [24]-имплементации
  • проверять изменения производительности бенчмарками

Эти шаги подходят для любой оптимизации, которую вам может потребоваться сделать.

Автор: dagen

Источник [25]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/javascript/265352

Ссылки в тексте:

[1] ES6 Прокси: https://habrahabr.ru/company/plarium/blog/267165/

[2] jsdom: https://github.com/tmpvar/jsdom

[3] Complink RPC library: https://github.com/GoogleChromeLabs/comlink

[4] полной спецификации: https://tc39.github.io/ecma262/#sec-proxy-objects

[5] CodeStubAssembler: https://github.com/v8/v8/wiki/CodeStubAssembler-Builtins

[6] вспомогательных объявлений-заглушек (helper predicates): https://github.com/v8/v8/blob/4e5db9a6c859df7af95a92e7cf4e530faa49a765/src/code-stub-assembler.h

[7] начальная версия: https://github.com/v8/v8/commit/f2af839b1938b55b4d32a2a1eb6704c49c8d877d#diff-ed49371933a938a7c9896878fd4e4919R97

[8] apply: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy/handler/apply

[9] construct: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy/handler/construct

[10] d8 shell: https://github.com/v8/v8/wiki/Building%20from%20Source

[11] get-перехватчик: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy/handler/get

[12] этом видео: https://www.youtube.com/watch?v=u7zRSm8jzvA

[13] has-перехватчика: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy/handler/has

[14] set-перехватчику: https://developer.mozilla.org/ru/docs/Web/JavaScript/Reference/Global_Objects/Proxy/handler/set

[15] именованными: https://v8project.blogspot.my/2017/08/fast-properties.html

[16] индексированными: https://v8project.blogspot.my/2017/09/elements-kinds-in-v8.html

[17] jsdom-proxy-benchmark: https://github.com/domenic/jsdom-proxy-benchmark

[18] ECMAScript specification: https://github.com/tc39/ecma262

[19] Ecmarkup: https://github.com/bterlson/ecmarkup

[20] jsdom@11.2.0: https://github.com/tmpvar/jsdom/blob/master/Changelog.md#1120

[21] v9.0.0-v8-canary-20170924: https://nodejs.org/download/v8-canary/v9.0.0-v8-canary20170924898da64843/node-v9.0.0-v8-canary20170924898da64843-linux-x64.tar.gz

[22] TimothyGu: https://github.com/TimothyGu

[23] Chai.js: https://v8project.blogspot.my/2017/10/chaijs.com

[24] TurboFan: https://github.com/v8/v8/wiki/TurboFan

[25] Источник: https://habrahabr.ru/post/339718/