Как-то раз, разглядывая CPU Profile, собранный в Chrome DevTools для нашего приложения, я заметил предупреждения на некоторых функциях о том, что они не были оптимизированы с причиной «Not optimized: Bad value context for arguments value».
Сама по себе проблема отключения оптимизации в Chrome богатая, но данная конкретная причина показалась странной. Гугление привело к этому посту, где автор утверждал, что предупреждение «Bad value context for arguments value» вызвано «неправильной работой с переменной arguments».
Я попытался разобраться в чем именно «неправильная работа» заключается, и насколько большое влияние это оказывает.
Для этого создал тест: http://jsperf.com/optimizing-arguments.
Этот тесткейз состоит из нескольких реализаций функции `append` (типа той, что в Underscore). Каждая реализация копирует `arguments` в массив. Первая реализация для этого использует классический подход с `Array.slice`. Вторая реализация («copy (Array allocated)») копирует `arguments` в цикле, непосредственно обращаясь по индексу к элементам. Тест «copy (Array unallocated)» вариант теста «copy (Array allocated)», только использует литерал [] вместо создания экземпляра `Array`. Последний тест «copy via helper function» копирует `arguments` с помощью вспомогательной функции.
И вот результаты:
Что мы видим. Трехкратное падение производительности при использовании Array.slice. Но не только. Тест с вспомогательной функцией демонстрирует такие же результаты. Т.е. похоже на то, что любая передача `arguments` куда-то вовне функции приводит к падению производительности. А `Array.slice` это просто частный случай общей проблемы.
Что же получается. Раз мы не можем передать `arguments` куда бы то ни было, то мы вынуждены писать тупой код копирования `arguments` с помощью for снова и снова. Жуть. Но есть хорошие новости. TypeScript спешит на помощь. В TypeScript есть т.н. rest parameters. Определение функции с rest-параметрами означает, что javascript код будет использовать `arguments`. Но делает он это правильно.
Следующий TS-код:
export function extend(obj, ...sources) {
sources.forEach(function (source) {
forEach(source, function (v, name) {
obj[name] = v;
});
});
return obj;
}
Cгенерирует:
function extend(obj) {
var sources = [];
for (var _i = 1; _i < arguments.length; _i++) {
sources[_i - 1] = arguments[_i];
}
sources.forEach(function (source) {
forEach(source, function (v, name) {
obj[name] = v;
});
});
return obj;
}
Отлично! То, что надо. Еще один маленький повод перейти на TypeScript.
P.S. Вот здесь полезный сборник причин отключения оптимизации в Chrome: https://github.com/GoogleChrome/devtools-docs/issues/53
Комментарии ()
dannyzubarev
25.05.2015 21:42Будет интересно почитать: github.com/petkaantonov/bluebird/wiki/Optimization-killers ;)
Zibx
25.05.2015 22:03Генерация функции в forEach должна сожрать намного больше чем слайс аргументов.
dom1n1k
26.05.2015 01:41+1Парадокс, но в реальности всё наоборот.
Я проводил тесты (в другом контексте, чем автор статьи, но вряд ли это существенно) — цикл forEach с генерацией/вызовом функции у меня работает существенно быстрее любого другого варианта цикла. Почему — не знаю.
Это в стародавние времена нас учили что вызов функции затратное мероприятие (положили в стек кучу всего, потом забрали), использовали inline в C++… Но в интерпретируемом языке, видимо, всё иначе.hell0w0rd
26.05.2015 11:55+1v8 не интерпретатор (а статья о нем), а jit компилятор. Поэтому есть операции, которые просто нельзя оптимизировать, самое популярное — arguments(все, кроме length и arguments[i]), eval, with.
IaIojek
26.05.2015 07:32До тех пор, пока arguments живёт внутри функции, это просто лексическая обёртка над механизмом обращения ко внутреннему хранилищу аргументов. Самого объекта, как бы, нет. Но как только вы попробуете передать это в другую функцию, вернуть через return, присвоить переменной, определённой выше текущего контекста, всё сразу же становится плохо. Создаётся сам объект arguments, если «повезёт», то ещё и контейнер под каждое значение.
Аналогичная ситуация с callee и caller.
Чтобы не терять производительность на пустом месте, лучше всего представлять, как какую-нибудь операцию вы реализовали бы на С/С++. Там есть rest-параметры, но никто не отдаёт их наружу. Однако, хоть это правило и покрывает много случаев, но остаются ещё некоторые специфические моменты, которые, всё таки, стоит прочитать в статьях по ссылкам вышеsymbix
26.05.2015 08:54С callee/caller все намного хуже — их наличие автоматически является запретом на оптимизации вида inline expansion. Причем, в отличие от arguments, нет ни одной разумной причины их использовать.
Aingis
26.05.2015 10:19+1Но при передаче через apply()-таки умеют оптимизовать. Да даже банальный (можно выделить в функцию, благодаря apply)
for (var a = new Array(arguments.length), i = 0; i < arguments.length; i++) a[i] = arguments[i];
будет быстрее чем Array#slice(). mrale.ph/blog/2015/04/12/jsunderhood.html
hell0w0rd
26.05.2015 11:51Какие-то выводы вы не правильные сделали, а в целом все так и есть. Возможно на хабре про это ничего нет, а вот на английском куча материала на эту тему.
vsb
26.05.2015 13:07А функция вида
function args_to_array() {
var result = [];
for (var i = 0; i < arguments.length; i++) result.push(arguments[i]);
return result;
}
используемая как
var arr = args_to_array.apply(null, arguments)
будет быстро работать?
Aetet
На самом деле нет:
babeljs.io/docs/learn-es2015/#default-rest-spread
Shrike
Простите, «нет» что?
Aetet
На самом деле не пора, т.к. и в ES6 будет-есть аналогичный функционал.