Производительность цикла JavaScript. Почему нужно уменьшить итератор на 0 быстрее, чем приращение

В своей книге Даже более быстрые веб-сайты Стив Саундерс пишет, что простой способ улучшить производительность цикла - уменьшить итератор до 0 а не увеличиваться по отношению к общей длине (на самом деле глава была написана Николасом К. Закасом). Это изменение может привести к экономии до 50% от первоначального времени выполнения, в зависимости от сложности каждой итерации. Например:

var values = [1,2,3,4,5];
var length = values.length;

for (var i=length; i--;) {
   process(values[i]);
}

Это почти идентично для цикла for, цикла do-while и цикла while.

Мне интересно, какая причина для этого? Зачем так быстро сокращать итератор? (меня интересует техническая информация об этом, а не в тестах, подтверждающих это утверждение.)


EDIT: На первый взгляд используемый здесь синтаксис цикла выглядит неправильно. Нет length-1 или i>=0, поэтому дайте понять (я тоже был смущен).

Вот общий синтаксис цикла:

for ([initial-expression]; [condition]; [final-expression])
   statement
  • начальное выражение - var i=length

    Это объявление переменной сначала оценивается.

  • условие - i--

    Это выражение оценивается перед каждой итерацией цикла. Он будет уменьшать переменную до первого прохождения через цикл. Если это выражение оценивается как false, цикл заканчивается. В JavaScript 0 == false, поэтому, если i окончательно равно 0, оно интерпретируется как false, и цикл завершается.

  • Окончательное выражение

    Это выражение оценивается в конце каждой итерации цикла (перед следующей оценкой состояния). Он здесь не нужен и пуст. Все три выражения являются необязательными в цикле for.

Синтаксис for loop не является частью вопроса, но, поскольку он немного необычен, мне кажется, что это интересно прояснить. И, возможно, одна из причин, по которой это происходит быстрее, потому что она использует меньше выражений (трюк 0 == false).

Ответ 1

Я не уверен в Javascript, и в современных компиляторах это, вероятно, не имеет значения, но в "старые времена" этот код:

for (i = 0; i < n; i++){
  .. body..
}

будет генерировать

move register, 0
L1:
compare register, n
jump-if-greater-or-equal L2
-- body ..
increment register
jump L1
L2:

в то время как код обратного отсчета

for (i = n; --i>=0;){
  .. body ..
}

будет генерировать

move register, n
L1:
decrement-and-jump-if-negative register, L2
.. body ..
jump L1
L2:

поэтому внутри цикла он выполняет только две дополнительные команды вместо четырех.

Ответ 2

Я считаю, что причина в том, что вы сравниваете конечную точку цикла с 0, которая быстрее сравнивает снова < length (или другую переменную JS).

Это потому, что порядковые операторы <, <=, >, >= являются полиморфными, поэтому этим операторам требуются проверки типов на левой и правой сторонах оператора, чтобы определить, какое поведение сравнения следует использовать.

Здесь есть очень хорошие тесты:

Какой самый быстрый способ кодировать цикл в JavaScript

Ответ 3

Легко сказать, что итерация может иметь меньше инструкций. Давайте просто сравним эти два:

for (var i=0; i<length; i++) {
}

for (var i=length; i--;) {
}

Когда вы подсчитываете каждый доступ к переменной и каждый оператор как одну команду, первый цикл for использует 5 инструкций (читайте i, читайте length, оценивайте i<length, test (i<length) == true, increment i), в то время как последний использует только 3 команды (прочитайте i, test i == true, декремент i). Это соотношение 5: 3.

Ответ 4

Как насчет использования обратного цикла while:

var values = [1,2,3,4,5]; 
var i = values.length; 

/* i is 1st evaluated and then decremented, when i is 1 the code inside the loop 
   is then processed for the last time with i = 0. */
while(i--)
{
   //1st time in here i is (length - 1) so it ok!
   process(values[i]);
}

IMO этот, по крайней мере, является более читаемым кодом, чем for(i=length; i--;)

Ответ 5

Я также изучал скорость цикла, и мне было интересно найти этот лакомый кусочек о том, что декремент быстрее, чем приращение. Тем не менее, мне еще предстоит найти тест, который демонстрирует это. На jsperf существует множество тестов цикла. Вот один из них, который проверяет декрементирование:

http://jsperf.com/array-length-vs-cached/6

Кэширование длины массива, однако (рекомендуется также книга Стива Соудера), похоже, является оптимизацией выигрышей.

Ответ 6

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

var array = [...];
var i = array.length;

for(;i--;) {
    do_teh_magic();
}

При этом вы пропустите даже проверку на [initial-expression]. Таким образом, вы закончите с одной операцией.

Ответ 7

for прирост против декремента в 2017 году

В современных JS-машинах инкрементирование в циклах for обычно быстрее, чем декремент (на основе личных тестов Benchmark.js), также более условно:

for (let i = 0; i < array.length; i++) { ... }

Это зависит от длины платформы и массива, если length = array.length имеет значительный положительный эффект, но обычно это не так:

for (let i = 0, length = array.length; i < length; i++) { ... }

Последние версии V8 (Chrome, Node) имеют оптимизацию для array.length, поэтому length = array.length может быть эффективно опущен там в любом случае.

Ответ 8

Я провел тест на С# и С++ (аналогичный синтаксис). Фактически, производительность существенно отличается от циклов for по сравнению с do while или while. В С++ производительность при увеличении увеличивается. Он также может зависеть от компилятора.

В Javascript, я считаю, все зависит от браузера (Javascript engine), но этого следует ожидать. Javascript оптимизирован для работы с DOM. Представьте себе, что вы просматриваете коллекцию элементов DOM, которые вы получаете на каждой итерации, и увеличиваете счетчик, когда вам нужно их удалить. Вы удаляете элемент 0, затем 1, но затем пропустите тот, который занимает место 0. При переходе назад эта проблема исчезает. Я знаю, что приведенный пример не только правильный, но я столкнулся с ситуациями, когда мне приходилось удалять элементы из постоянно меняющейся коллекции объектов.

Поскольку обратный цикл чаще всего неизбежен, чем прямой цикл, я предполагаю, что JS-движок оптимизирован именно для этого.

Ответ 9

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

for(var i = 0; i < array.length; i++) {
 ...
}

Фактически вы получаете доступ к свойству length массива N (количество элементов) раз. Если вы уменьшаете, вы получаете доступ к нему только один раз. Это может быть причиной.

Но вы также можете записать цикл инкремента следующим образом:

for(var i = 0, len = array.length; i < len; i++) {
 ...
}

Ответ 10

Вы приурочили себя? Г-н Саундерс может ошибаться в отношении современных переводчиков. Это точно такая оптимизация, в которой хороший автор компилятора может иметь большое значение.

Ответ 11

в современных JS-двигателях разница между прямыми и обратными контурами почти не существует. Но разница в производительности сводится к двум вещам:

a) дополнительный поиск каждого свойства длины каждый цикл

//example:
    for(var i = 0; src.length > i; i++)
//vs
    for(var i = 0, len = src.length; len > i; i++)