Оптимизация JavaScript: Scope, Low level ES vs ES5 Array methods

    Сегодня мы будем тестировать 2 блока кода, выполняющие следующую операцию:
    Дается массив, необходимо выбрать все элементы, степень 2 которых больше 5.

    В синем углу Вариант А: Низкоуровневый код — старый и страшный (поддающийся частичной оптимизации)
    1.    for (i = 0, res = []; i < c; i++) {
    2.        t = a[i];
    3.        if (t >= 2.236067) {
    4.            continue;
    5.        } else {
    6.            res.push(t * t);
    7.        }
    8.    }

    В красном углу Вариант Б: Высокоуровневый код — молодой и красивый (не поддающийся частичной оптимизации)
    1. a.map(function (t) { return t * t}).filter(function (t) { return t > 5});

    Битвы будут происходить на 3 аренах.
    1. AO args — Параметры объекта активации функции.
    2. AO — Локальные переменные объекта активации функции.
    3. Global — Глобальные переменные.



    В арсенале у нас последние стабильные версии всех популярных браузеров.

    Весь код теста:
    1.  
    2. // * * * * * * * * * * * * * * * * *
    3. // Activation object Arguments scope
    4. // * * * * * * * * * * * * * * * * *
    5.  
    6. (function (a, dt, index, i, c, r, t, res) {
    7. r = [];
    8. c = a.length;
    9.  
    10. dt = new Date();
    11. index = 20000;
    12. while (index--) {
    13.    a.map(function (t) { return t * t}).filter(function (t) { return t > 5});
    14. }
    15. r[0] = (new Date() - dt);
    16.  
    17. dt = new Date();
    18. index = 20000;
    19. while (index--) {
    20.    for (i = 0, res = []; i < c; i++) {
    21.        t = a[i];
    22.        if (t >= 2.236067) {
    23.            continue;
    24.        } else {
    25.            res.push(t * t);
    26.        }
    27.    }
    28. }
    29. r[1] = (new Date() - dt);
    30.  
    31. alert('ao args: ' + r);
    32. }([1,2,3,4,5,6,7,8,9,10,1,2,3,4,5,6,7,8,9,10,1,2,3,4,5,6,7,8,9,10,1,2,3,4,5,6,7,8,9,10,1,2,3,4,5,6,7,8,9,10]));
    33.  
    34. // * * * * * * * * * * * * * * * * *
    35. // Activation object scope
    36. // * * * * * * * * * * * * * * * * *
    37.  
    38. (function () {
    39. var a = [1,2,3,4,5,6,7,8,9,10,1,2,3,4,5,6,7,8,9,10,1,2,3,4,5,6,7,8,9,10,1,2,3,4,5,6,7,8,9,10,1,2,3,4,5,6,7,8,9,10],
    40.     dt,
    41.     index,
    42.     i,
    43.     c,
    44.     r,
    45.     t,
    46.     res
    47.     ;
    48.  
    49. r = [];
    50. c = a.length;
    51.  
    52. dt = new Date();
    53. index = 20000;
    54. while (index--) {
    55.    a.map(function (t) { return t * t}).filter(function (t) { return t > 5});
    56. }
    57. r[0] = (new Date() - dt);
    58.  
    59. dt = new Date();
    60. index = 20000;
    61. while (index--) {
    62.    for (i = 0, res = []; i < c; i++) {
    63.        t = a[i];
    64.        if (t >= 2.236067) {
    65.            continue;
    66.        } else {
    67.            res.push(t * t);
    68.        }
    69.    }
    70. }
    71. r[1] = (new Date() - dt);
    72.  
    73. alert('ao:      ' + r);
    74. }());
    75.  
    76. // * * * * * * * * * * * * * * * * *
    77. // Global scope
    78. // * * * * * * * * * * * * * * * * *
    79.  
    80. var a = [1,2,3,4,5,6,7,8,9,10,1,2,3,4,5,6,7,8,9,10,1,2,3,4,5,6,7,8,9,10,1,2,3,4,5,6,7,8,9,10,1,2,3,4,5,6,7,8,9,10],
    81.     dt,
    82.     index,
    83.     i,
    84.     c,
    85.     r,
    86.     t,
    87.     res
    88.     ;
    89.  
    90. r = [];
    91. c = a.length;
    92.  
    93. dt = new Date();
    94. index = 20000;
    95. while (index--) {
    96.    a.map(function (t) { return t * t}).filter(function (t) { return t > 5});
    97. }
    98. r[0] = (new Date() - dt);
    99.  
    100. dt = new Date();
    101. index = 20000;
    102. while (index--) {
    103.    for (i = 0, res = []; i < c; i++) {
    104.        t = a[i];
    105.        if (t >= 2.236067) {
    106.            continue;
    107.        } else {
    108.            res.push(t * t);
    109.        }
    110.    }
    111. }
    112. r[1] = (new Date() - dt);
    113.  
    114. alert('global:  ' + r);
    115.  


    Код: pastebin.com/mqBdkXZG

    Результаты


    FF high level low level
    AO args 458 92
    AO 474 122
    Global 479 162
    Opera high level low level
    AO args 416 22
    AO 427 21
    Global 428 49
    Chrome high level low level
    AO args 83 9
    AO 89 8
    Global 98 28
    Sa high level low level
    AO args 153 21
    AO 146 24
    Global 147 25
    IE8 high level low level
    AO args выбыл 441
    AO выбыл 393
    Global выбыл 822

    Итоги


    Как видим, наблюдается тенденция AO args|AO быстрее Global. Это ясно из спецификации ECMAScript — обращение к переменным объекта активации быстрее потому, что он(АО) лежит «ближе к коду» чем глобальный объект.
    Низкоуровневый код, в разы быстрее всокоуровневого потому что в ECMAScript нет блоков как в Ruby, для каждого элемента массива вызывается функция, а вызов функции затратная операция для JS. Высокоуровневый код медленнее до 20 раз!
    Chrome такой быстрый потому что имеет JIT компилляцию, но для часто используемых блоков кода (1 прогон даст одинаковый результат).
    Интересный момент показывает Firefox: AO args (low) 92; AO (low) 122; AO args на четверть быстрее. Хотя все эти переменные что AO args, что AO лежат в одном объекте, но в ФФ AO args, судя по результатам, выделяется в отдельный объект.

    Ещё интересный тест.
    1. (function (r, dt, index, i, j) {
    2.  
    3. dt = new Date();
    4. index = 50000;
    5. while(index--) {
    6.  
    7. // Block A
    8.   for (i = 0, j= 0; i < 20; i++) {
    9.     j++;
    10.   }
    11. // -------
    12.  
    13. }
    14. r[0] = new Date - dt;
    15.  
    16. dt = new Date();
    17. index = 50000;
    18. while(index--) {
    19.  
    20. // Block B
    21.   j = 0;
    22.   j++;j++;j++;j++;j++;
    23.   j++;j++;j++;j++;j++;
    24.   j++;j++;j++;j++;j++;
    25.   j++;j++;j++;j++;j++;
    26. // -------
    27.  
    28. }
    29. r[1] = new Date - dt;
    30.  
    31. alert(r);
    32.  
    33. }([]));


    Какой блок будет быстрее? Ответ: pastebin.com/hXxQb6pk

    UPD В конечном итоге все упирается перерисовку интерфейса (reflow, redraw), однако, как показала моя практика, оптимизации reflow было не достаточно. Значительную часть съедали вызовы анонимных функций, устранив их был получен хороший прирост, особенно в древних браузерах, из-за которых мы и страдаем. Не воспринимайте эту статью как руководство к действию, пишите код как считаете удобным для вас. Я советую производить оптимизацию по необходимости.
    Поделиться публикацией
    Похожие публикации
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама
    Комментарии 29
    • +1
      [ t*t for each( t in a ) if t > 2.236067 ]
      • +1
        Прошу прощения, что не в ту ветку, а повыше, но новичкам необходимо трезво оценивать статью автора, а не бездумно верить в этот далекий от истины текст

        Автор — лукавит и вот почему.
        1. При использовании коллбеков сначала проиходит возведение в квадрат, а потом — фильтрация. При использовании цикла сначала происходит фильтрация, а потом — умножение
        2. У автора получается различный результат. Коллбек возвращает в 4 раза больше данных.
        3. Результат просто сгорает. Вполне возможно, что браузеры оптимизируют этот кусок и потому надо присваивать результат глобальной переменной.
        4. Переменные объявляются вне цикла, хотя это тоже должно быть частью теста
        5. Длина массива тоже получается вне цикла, хотя оно тоже должно быть частью теста (c = a.length)
        6. Во всех тестах идёт работа с одним и тем же массивом. Стоит создавать одинаковые массивы для каждого теста, потому что могут быть оптимизации.

        Если соблюсти объективность и корректность тестов, то различия — несущественны. В один-два раза, а не в 20 раз, как врёт автор.
        В некритичных местах такая разница не влияет на производительность.
        На практике это не может быть узким местом. Тем более, вариант с коллбеками действительно читабельнее

        Но все-равно стоит знать об єтом и только в очень требовательных к производительности местах переводить на циклы

        /** Результаты:
         * 
         * Firefox 4beta:
         *   loop: 115
         *   clbk: 184
         *
         * Chrome 8:
         *   loop: 53
         *   clbk: 60
         */
        var loop = function (array) {
           var result = [];
           for (var i = 0, c = array.length; i < c; i++) {
              var t = array[i];
              if (t < 2.236067) {
                 result.push(t * t);
              }
           }
           return result;
        };
        
        var callback = function (array) {
           return array
              .filter(function (t) {
                 return t < 2.236067;
              })
              .map(function (t) {
                 return t * t;
              });
        };
        
        var createArray = function () {
           return [1,2,3,4,5,6,7,8,9,10,1,2,3,4,5,6,7,8,9,10,1,2,3,4,5,6,7,8,9,10,1,2,3,4,5,6,7,8,9,10,1,2,3,4,5,6,7,8,9,10];
        };
        
        var index, time = {}, start;
        
        start = Date.now();
        for (index = 20000; index--;) window.test_lp = loop(createArray());
        time.loop = Date.now() - start;
        
        start = Date.now();
        for (index = 20000; index--;) window.test_cb = callback(createArray());
        time.clbk = Date.now() - start;
        
        alert('loop: ' + time.loop + '\n' + 'clbk: ' + time.clbk);
        
        


        Между прочим, согласно новому синтаксису можно записывать так (хотя это и поддерживается не всеми браузерами, например Фоксом, но не Хромом):
        var callback = function (array) {
           return array
              .map   ( function (t) t * t; )
              .filter( function (t) t > 5; )
        };
        
        • –2
          В третий раз пишу что тут тестирование разных подходов — высокоуровневый vs низкоуровневый (эксперимент не чист я признаю), прочите, плиз, комменты до конца.

          > не в 20 раз, как врёт автор.
          Я не говорил что в 20 раз. Медленнее до 20 раз в сафари (от 3 раз в фф)

          > Результат просто сгорает.
          Не повлияет на конечный результат.

          > Переменные объявляются вне цикла, хотя это тоже должно быть частью теста
          Однако место их объявления решает исход теста.

          Верить хрому не стоит у него JIT компиллер он читерит в бенчмарках.
          Вы в своих бенчмарках делаете многое лишнего — 40к вызовов левых функций, которые растворяют результат.
          • +1
            В третий раз пишу что тут тестирование разных подходов — высокоуровневый vs низкоуровневый (эксперимент не чист я признаю), прочите, плиз, комменты до конца.

            Ну я и говорю, что тестирование неверное. Давайте протестируем 1+1 на низкоуровневом подходе и 10000! на высокоуровневом — вообще будет разница ошеломляющая, тысячи раз. Сравнивайте одинаковые условия и одинаковый результат

            Я не говорил что в 20 раз. Медленнее до 20 раз в сафари (от 3 раз в фф)

            Я вижу разницу в Сафари в 6,5 раз (150 vs 23), а не в 20 раз. Самая большая разница — в Опере — в 13 раз. Даже на этих нечесных тестах.

            > Результат просто сгорает.
            Не повлияет на конечный результат.

            Влияет

            Однако место их объявления решает исход теста.

            Именно об этом я и говорю. Вы хотите протестировать два разных подхода. Так вот — неотемлимая часть низкоуровневого — получение длины массива. Невозможно выполнить код, не посчитав длину массива, а вы вынесли этот подсчёт из теста, уменьшив время выполнения. Может еще и фильтрацию вынесете?

            Верить хрому не стоит у него JIT компиллер он читерит в бенчмарках.

            Именно потому он может считерить в ваших тестах, выдав результат в 9 мс — там есть много мест для вырезания и оптимизации, которые в реальном проекте не появятся. Например, тот факт, что результат нигде не используется.

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

            Важно проводить тесты в песочнице, чтобы разные факторы не влияли на результат. На практике вы не пишете код в одном потоке, а используете функции.
          • 0
            Даже если это будет быстрее в один-два раза, разве это плохо? Я бы сказал совсем зажрались. Системные программисты рады и 10% ускорению, а нам и 2 раз мало… по капле и ведро набежит.
            • 0
              Так, погодите, не занимайтесь демагогией. Где я говорил, что ускорение в 10% это плохо?
              Ускорение в доли процента (а на уровне проекта это выльется в доли процента, потому что узкое место — рефлоу или отрисовка в канвас) за счет значительного ухудшения читабельности — это плохо.

              У системных программистов совсем другая область.
              • 0
                Согласен, что в конечном итоге все упирается перерисовку интерфейса, однако, как показала моя практика, оптимизации reflow было не достаточно. Значительную часть съедали вызову анонимных функций, устранив их был получен хороший прирост, особенно в древних браузерах, из-за них мы и страдаем.

                > значительного ухудшения читабельности
                При хороший организации документации и адекватном наименовании функций отпадает необходимость читать код. Инкапсуляция решает.
          • 0
            Кстати, tenshi, тестировал и твой вариант, но в Хроме он не заработал. В фоксе 3.5 он сравнялся по скорости с «низкоуровневым», в то время, как в Фоксе 4бета он сравнялся с «высокоуровневым»
            • 0
              Не забывайте, что не хромом и Ффоксом едины.
          • +4
            Я правильно понимаю, что low-level версия возводит в квадрат уже отфильтрованные значения, а high-level все подряд и потом фильтрует?
            • +2
              А что, для кого-то все это — сюрприз? Серьезно? К тому же большинство подобных синтетических тестов *ни*о*чем* не говорят. Вообще. Основное узкое место — reflow, а подобными спичечными оптимизациями ни один программист в здравом уме заниматься не будет.
              • 0
                > А что, для кого-то все это — сюрприз? Серьезно?
                Разве для вас не сюрприз, что в FF тест [AO args] быстрее теста [AO]?

                Забили тут, забили здесь: -10% -20% к производительности. Я не призываю оптимизировать с самого начала и сидеть над профайлером над каждыми блоком, я советую сразу отказываться от высокоуровневых операций для тяжелых вычислений — это потенциальное узкое место вашего проекта.
                • +3
                  Да какие, нахрен, проценты тут могут быть? Возьмите реальное приложение и посмотрите профайлером насколько заметен будет эффект от подобных мелочей.

                  Так что не занимайте фигней и делайте оптимизаци на уровне алгоритмов. Если у вас действительно узкое место — язык, то меняйте технологию. Да, не все можно и нужно делать в браузере. А данные примеры как раз про узкие места языка.

                  > Сразу отказываться от высокоуровневых операций <...>
                  Читайте умные книжки и хватит заниматься преждевременной оптимизацией. Жертвовать читабельностью кода (а в JS она и так не супер) ради пары миллисекунд это супер

                  А то, блядь, понавидался я статей от похапешников из серии «что быстрее print или echo». Теперь еще и JS…

                  С вашим пониманием процесса оптимизации писать подобные статьи нельзя. Процесс оптимизации может быть только один — взять готовое приложение и пройтись профайлером. Очень надеюсь что никакие новички не воспримут ваши слова как руководство к действию.
                  • 0
                    > Сразу отказываться от высокоуровневых операций
                    Сразу отказываться от высокоуровневых операций для тяжелых вычислений (Например анимация)

                    > С вашим пониманием процесса оптимизации писать подобные статьи нельзя.
                    Как ни странно мы с вами одинаково понимаем процесс оптимизации js — профилировение, по результатом нахождение узких мест, правка функций/алгоритмов.

                    > хватит заниматься преждевременной оптимизацией
                    Преждевремення оптимизация это когда сидишь над одним блоком кода и думаешь будет быстрее так или так. Тут же совсем иное, другой подход, который в корне оптимальный.

                    > Жертвовать читабельностью кода
                    Это зачастую оборотная сторона оптимизации.
              • +4
                Какая-то ерунда, тестируются совершенно разные операции.
                • 0
                  Это 2 разных подхода, операции же возвращают одинаковый результат.
                  • +2
                    Это 2 разных подхода, операции же возвращают одинаковый результат:

                    x = 5 + 5;
                    

                    x = mysql("SELECT 5+5");
                    
                    • 0
                      Да, это мы и тестируем. Программисты зачастую склонны к новому. ES5 Arrays один их тех примеров которые сейчас активно используются. Да красиво, более наглядно, но по скорости исполнения в разы медленнее низкоуровнего подхода.
                      • 0
                        Вы похоже не поняли. Вы тестируете совершенно разные операции. Мой пример показывает, что можно использовать любые операции для достижения одного результата и это не повод их сравнивать.
                        • 0
                          Я понял, что вы имеете в виду. Очевидно, что это совершенно разные операции, разные подходы к решению данной задачи.
                          Сейчас все больше программистов пишет на высокоуровневом js (модно, круто, ново). Моим тестом я пытаюсь предостеречь от злоупотребления ES5 Arrays.
                • –2
                  «Высокоуровневый, красивый..»
                  Если бы кто-нибудь заменил первый пример через эту «красоту» в моём проекте, не дай бог высоконагруженном, я бы сразу уволил.
                  • 0
                    Хабр съел комменты, после того как запись побывала в черновике. Отвечаю по памяти.
                    > Почему написан фактически разный код?
                    Даже если бы я убрал хак с if (t >= 2.236067) то ничего бы не изменилось, да и по сути то, что есть это 2 совершенно разных подхода и мы их тестируем.
                    Плюсы низкоуровнего подхода в том, что он поддается частичной оптимизации, в то время как высокоуровневый подход нет.

                    • 0
                      > коммент о том, что восновном тормозят reflow
                      Мы делаем анимацию объекта (70-100 операций в секунду для одного бъекта) через высокоуровневый итератор (forEach по элементами forEach по свойствам), но у нас оптимизированный reflow, например объект вытащили в absolute. В этом случае будет создавать тормоза каскад forEach, а не reflow. Я согласен, что бывают узкие места и reflow один из них, но часто требуется комплескный подход к вопросу. Особенно это касается оптимизации циклов.
                      • +1
                        кстати у вас ошибка — один код возвращает «все элементы, степень 2 которых больше 5», а другой(низкоуровневый) все элементы, степень 2 которых МЕНЬШЕ 5.
                        • +1
                          замена вашего высокоуровневого на альтернативный вариант улучшает время в 1.5 раза:
                             var res = [];
                             a.forEach(function (t) { 
                               if(t >= 2.236067)
                                  res.push(t * t);
                              });
                          
                          • 0
                            Сократили 51 вызов функции, но до низкоуровнего кода ещё далеко. Насчет меньше 5 вы правы, но это не сильно повлияет на результат.
                            • +1
                              Только в одном случае вы возвращаете результат res, а в другом нет. В разных вариантах вы получаете разные результаты, идете по разной логике. Назвать это корректным тестированием нельзя ни в коем разе. Все выводы будут на глазок. Для такого простого случая оно и так было все понятно, а в сложном случае доверия вам не будет совсем.
                              • 0
                                Я тестирую 2 разных подхода (hight/low level). И показываю, что высокоуровневый подход в разы медленнее чем низкоуровневый. Тут не столько дело в возвращении не возвращении результата и моей ошибке со знаком в первом, сколько именно в сравнении подхода (hight/low level). Хотя для чистоты эксперимента стоило бы поправить.
                                > ES5 Arrays один их тех примеров, которые сейчас активно используются. Да красиво, более наглядно, но по скорости исполнения в разы медленнее низкоуровнего подхода.
                          • 0
                            node.js 0.3.3 (V8 3.x):

                            ao args: [ 96, 14 ]
                            ao: [ 99, 14 ]
                            global: [ 94, 14 ]

                            node.js 0.2.6:

                            ao args: [ 95, 13 ]
                            ao: [ 89, 13 ]
                            global: [ 91, 14 ]

                            Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.