Disclaimer: Я уверен, что это баян. Но быстрый поиск не нашел на Хабре аналогичной статьи. Если есть, пишете в комментах, удалю.

Как-то раз, разглядывая 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` с помощью вспомогательной функции.

И вот результаты:

image

Что мы видим. Трехкратное падение производительности при использовании 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

Комментарии ()


  1. Aetet
    25.05.2015 20:52
    +2

    На самом деле нет:
    babeljs.io/docs/learn-es2015/#default-rest-spread


    1. Shrike
      25.05.2015 21:11

      Простите, «нет» что?


      1. Aetet
        25.05.2015 22:10
        +4

        На самом деле не пора, т.к. и в ES6 будет-есть аналогичный функционал.


  1. dannyzubarev
    25.05.2015 21:42

    Будет интересно почитать: github.com/petkaantonov/bluebird/wiki/Optimization-killers ;)


  1. Zibx
    25.05.2015 22:03

    Генерация функции в forEach должна сожрать намного больше чем слайс аргументов.


    1. dom1n1k
      26.05.2015 01:41
      +1

      Парадокс, но в реальности всё наоборот.
      Я проводил тесты (в другом контексте, чем автор статьи, но вряд ли это существенно) — цикл forEach с генерацией/вызовом функции у меня работает существенно быстрее любого другого варианта цикла. Почему — не знаю.
      Это в стародавние времена нас учили что вызов функции затратное мероприятие (положили в стек кучу всего, потом забрали), использовали inline в C++… Но в интерпретируемом языке, видимо, всё иначе.


      1. hell0w0rd
        26.05.2015 11:55
        +1

        v8 не интерпретатор (а статья о нем), а jit компилятор. Поэтому есть операции, которые просто нельзя оптимизировать, самое популярное — arguments(все, кроме length и arguments[i]), eval, with.


  1. IaIojek
    26.05.2015 07:32

    До тех пор, пока arguments живёт внутри функции, это просто лексическая обёртка над механизмом обращения ко внутреннему хранилищу аргументов. Самого объекта, как бы, нет. Но как только вы попробуете передать это в другую функцию, вернуть через return, присвоить переменной, определённой выше текущего контекста, всё сразу же становится плохо. Создаётся сам объект arguments, если «повезёт», то ещё и контейнер под каждое значение.
    Аналогичная ситуация с callee и caller.

    Чтобы не терять производительность на пустом месте, лучше всего представлять, как какую-нибудь операцию вы реализовали бы на С/С++. Там есть rest-параметры, но никто не отдаёт их наружу. Однако, хоть это правило и покрывает много случаев, но остаются ещё некоторые специфические моменты, которые, всё таки, стоит прочитать в статьях по ссылкам выше


    1. symbix
      26.05.2015 08:54

      С callee/caller все намного хуже — их наличие автоматически является запретом на оптимизации вида inline expansion. Причем, в отличие от arguments, нет ни одной разумной причины их использовать.


    1. 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


  1. hell0w0rd
    26.05.2015 11:51

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


    1. Shrike
      26.05.2015 12:15

      В чем неправильность выводов?


      1. hell0w0rd
        26.05.2015 13:54

        Вам выше уже объяснили, эта возможность будет добавлена в es6, а прямо сейчас es6 можно использовать с помощью babel.


  1. 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)

    будет быстро работать?


    1. Shrike
      26.05.2015 13:23