- PVSM.RU - https://www.pvsm.ru -
Это перевод поста из официального блога 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);
После портирования вызова перехватчика на CSA все вызовы происходят в JS-среде, уменьшая количество "прыжков" между языками с 4-х до нуля.
Это изменения привело к следующему улучшению производительности:
Наши измерения производительности JS показывают ускорение от 49% до 74%. Грубо говоря мы измеряли, сколько раз конкретный микробенчмарк может быть запущен за 1000 мс. Для некоторых тестов код запускается несколько раз, чтобы уточнить результат (из-за ограниченной точности таймера). Код всех бенчмарков ниже может быть найден в нашей js-perf-test директории.
Call
и construct
перехватчикиСледующая часть показывает результаты оптимизации перехватчиков вызова и создания (они же apply
[8] и construct
[9]).
Значительное увеличение производительности при вызове прокси — до 500% быстрее! А ускорение создания прокси не так примечательно, особенно если не определены никакие перехватчики — в этом случае ускорение только 25%. Получили мы эти результаты с помощью запуска следующей команды в d8 shell
[10]:
Где 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
-перехватчик [11] более запутан, чем предыдущие примеры, из-за особенностей поведения инлайн-кеша. Подробнее про инлайн-кеши вы можете посмотреть в этом видео [12].
В конечном счёте мы получили рабочий порт на CSA со следующими результатами:
После применения изменений мы заметили, что размер apk-файла Chrome для Android вырос на ~160Kb, что больше ожидаемого для маленькой функции в 20 строчек, но, к счастью, мы храним подобную статистику. Оказалось, что функция вызывается дважды из другой функции, которая вызывается 3 раза из третьей, которая вызывается 4 раза. Причиной проблемы был агрессивный инлайн функций. В конце концов мы решили проблему вынесением функции в отдельную заглушку (здесь, видимо, имеются ввиду те же заглушки [6], что выше были названы "предикатами"), что сохранило драгоценные килобайты — окончательная версия увеличивала объём apk-файла всего лишь на ~19Kb.
Следующая часть показывает результаты оптимизации has
-перехватчика [13]. Мы думали это будет легко (ожидали переиспользования большей части кода из get-перехватчика), но у has
своя атмосфера. Отчасти из-за трудно отлаживаемой проблемы обхода цепочки прототипов при вызове in
оператором. Результаты улучшения варьируются от 71% от 428%. И опять выигрыш более заметен, если перехватчики определены при создании.
Теперь мы переходим к set
-перехватчику [14]. И в этот раз нам надо по-разному работать с именованными [15] и индексированными [16] свойствами (элементами). Эти два типа не часть языка JS, а результат внутренних оптимизаций обработки свойств объектов. Изначальная реализация прокси по-прежнему выходит из среды исполнения (для элементов), что опять приводит к пересечению сред исполнения. Тем не менее мы достигли улучшения от 27% до 438% для случаев, когда перехватчик определён, но ценой замедления на 23%, если не определён. Падение производительности здесь обусловлено дополнительными проверками, чтобы различать индексированные и именованные свойства объекта. Для индексированных свойств пока нет никаких улучшений. Вот график с полными результатами:
Полученные в jsdom-proxy-benchmark [17]:
Проект jsdom-proxy-benchmark составляет (в прямом смысле слова составляет: собирает в один html-файл) ECMAScript specification [18] с помощью инструмента Ecmarkup [19]. В версии jsdom@11.2.0 [20] (который лежит в основе Ecmarkup) использует прокси для реализации таких структур как NodeList
и HTMLCollection
. Мы использовали это как бенчмарк, чтобы измерить прозводительность в более приближенном к реальному миру сценарии, чем наши синтетические микро-бенчмарки. За 100 проходов средние результаты такие:
Спасибо за результаты, предоставленные TimothyGu [22].
Полученные в Chai.js [23]:
Chai.js — это популярная библиотека ассертов, довольно плотно использующая прокси. Мы сделали что-то типа бенчмарка, использующего реальные сценарии; и запуская тесты под разные версии V8 выявили выигрыш более чем одной секунды из четырёх. В среднем за 100 запусков:
У нас есть прижившийся стандартный подход, как побеждать узкие места производительности, и краеугольный камень — это следующие несколько шагов (которыми мы и следовали в раскрытой в этой статье работе):
Эти шаги подходят для любой оптимизации, которую вам может потребоваться сделать.
Автор: 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/
Нажмите здесь для печати.