Циклы действительно быстрее в обратном направлении?


268

Я слышал это довольно много раз. Действительно ли циклы JavaScript действительно быстрее при обратном отсчете? Если так, то почему? Я видел несколько примеров набора тестов, показывающих, что обратные циклы быстрее, но я не могу найти объяснения, почему!

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

Т.е.

for (var i = count - 1; i >= 0; i--)
{
  // count is only evaluated once and then the comparison is always on 0.
}

6
хе - хе. это займет неопределенно. попробуйте я ...
StampedeXV

5
forЦикл в обратном направлении быстрее, потому что управляющая переменная цикла верхней (нижней, нижней) петли не должна быть определена или извлечена из объекта; это постоянный ноль.
Джош Стодола

88
Там нет никакой разницы . Собственные циклические конструкции всегда будут очень быстрыми . Не беспокойтесь об их производительности.
Джеймс Аллардис

24
@Afshin: По таким вопросам, пожалуйста, покажите нам статьи, на которые вы ссылаетесь.
Гонки легкости на орбите

22
Разница в основном важна для устройств с очень низким уровнем заряда и питанием от батарей. Разница в том, что с i-- вы сравниваете с 0 для конца цикла, в то время как с i ++ вы сравниваете с числом> 0. Я считаю, что разница в производительности составляла примерно 20 наносекунд (что-то вроде cmp ax, 0 против cmp ax , bx) - это не что иное, как если бы вы повторяли тысячи раз в секунду, почему бы не получить усиление в 20 наносекунд для каждого :)
luben

Ответы:


903

Это не так i--быстрее, чем i++. На самом деле, они оба одинаково быстры.

В восходящих циклах требуется время, чтобы оценить для каждого iразмер вашего массива. В этом цикле:

for(var i = array.length; i--;)

Вы оцениваете .lengthтолько один раз, когда вы объявляете i, тогда как для этого цикла

for(var i = 1; i <= array.length; i++)

Вы оцениваете .lengthкаждый раз i, когда вы увеличиваете , когда вы проверяете, если i <= array.length.

В большинстве случаев вам даже не стоит беспокоиться об этом виде оптимизации .


23
Стоит ли вводить переменную для array.length и использовать ее в заголовке цикла for?
Рагацкинет

39
@ragatskynet: Нет, если вы не устанавливаете эталонный тест и не хотите что-то сделать.
Джон

29
@ragatskynet Это зависит от того, будет ли быстрее вычислять .lengthопределенное количество раз или объявлять и определять новую переменную? В большинстве случаев это преждевременная (и неправильная) оптимизация, если только ваша lengthоценка не слишком дорога.
Алестанис

10
@ Dr.Dredel: Это не сравнение - это оценка. 0быстрее оценить, чем array.length. Ну, предположительно.
Гонки легкости на орбите

22
Стоит отметить, что речь идет о интерпретируемых языках, таких как Ruby, Python. Скомпилированные языки, например Java, имеют оптимизацию на уровне компилятора, которая «сгладит» эти различия до такой степени, что не имеет значения, .lengthнаходится ли он в объявлении for loopили нет.
Харис Краина

198

Этот парень сравнивал множество циклов в javascript, во многих браузерах. У него также есть набор тестов, так что вы можете запустить их самостоятельно.

Во всех случаях (если я не пропустил ни одного в моем чтении) самый быстрый цикл был:

var i = arr.length; //or 10
while(i--)
{
  //...
}

Хорошо :) Но нет проверенного обратного цикла "for" ... Но циклы for, упомянутые peirix или Searlea, должны быть почти такими же, как цикл "while" с "i--" в качестве условия. И это была самая быстрая из проверенных петель.
SvenFinke

11
Интересно, но мне интересно, будет ли предварительный декремент еще быстрее. Так как не нужно хранить промежуточное значение i.
tvanfosson

Если я правильно помню, мой профессор аппаратного курса сказал, что тест на 0 или не 0 - это самый быстрый «расчет». В то время как (i--) тест всегда тест на 0. Может быть, поэтому он самый быстрый?
StampedeXV

3
@tvanfosson, если вы предварительно уменьшите значение, --iтогда вы должны использовать var current = arr[i-1];внутри цикла, иначе он будет выключен одним ...
DaveAlger,

2
Я слышал, что i-- может быть быстрее, чем --i, потому что во втором случае процессор должен уменьшать и затем проверять новое значение (между инструкциями есть зависимость данных), тогда как в первом случае процессор может проверить существующее значение и уменьшить значение через некоторое время. Я не уверен, относится ли это к JavaScript или только к очень низкоуровневому коду.
Маркус

128

Я пытаюсь дать общую картину с этим ответом.

Следующие мысли в скобках были моей верой, пока я только недавно не проверил проблему:

[[В терминах языков низкого уровня, таких как C / C ++ , код скомпилирован так, что процессор имеет специальную команду условного перехода, когда переменная равна нулю (или не равна нулю).
Кроме того, если вам небезразлична такая большая оптимизация, вы можете пойти ++iвместо нее i++, потому что ++iэто однопроцессорная команда, а i++значит j=i+1, i=j.]]

По-настоящему быстрые петли можно сделать, развернув их:

for(i=800000;i>0;--i)
    do_it(i);

Это может быть намного медленнее, чем

for(i=800000;i>0;i-=8)
{
    do_it(i); do_it(i-1); do_it(i-2); ... do_it(i-7);
}

но причины этого могут быть довольно сложными (просто упомянуть, что в игре есть проблемы с предварительной обработкой команд процессора и обработкой кеша).

С точки зрения языков высокого уровня , таких как JavaScript, как вы просили, вы можете оптимизировать вещи, если полагаетесь на библиотеки, встроенные функции для зацикливания. Пусть решают, как это лучше всего сделать.

Следовательно, в JavaScript я бы предложил использовать что-то вроде

array.forEach(function(i) {
    do_it(i);
});

Это также менее подвержено ошибкам, и браузеры имеют возможность оптимизировать ваш код.

[ЗАМЕЧАНИЕ: не только браузеры, но у вас также есть место для легкой оптимизации, просто переопределите forEachфункцию (в зависимости от браузера), чтобы она использовала новейшие хитрости! :) @AMK говорит, что в особых случаях стоит использовать array.popor array.shift. Если вы сделаете это, положите его за занавес. Самое лишнее - добавить опции forEachдля выбора алгоритма зацикливания.]

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

Эти библиотеки также могут помещать вещи (многопоточные) за вашу спину, а также специализированные программисты поддерживают их актуальность.

Я провел более тщательную проверку, и оказалось, что в C / C ++, даже для операций 5e9 = (50,000x100,000), нет никакой разницы между повышением и понижением, если тестирование выполняется с константой, как говорит @alestanis. (Результаты JsPerf иногда противоречивы, но, по большому счету, говорят одно и то же: вы не можете иметь большого значения.)
Так --iчто это скорее «шикарная» вещь. Это только делает вас похожим на лучшего программиста. :)

С другой стороны, для разворачивания в этой ситуации 5e9 это снизило меня с 12 секунд до 2,5 секунд, когда я шел на 10 секунд, и до 2,1 секунд, когда я шел на 20 секунд. Это было без оптимизации, а оптимизация привела к невообразимому небольшому времени. :) (Развертывание может быть сделано моим способом выше или с использованием i++, но это не продвигает вещи вперед в JavaScript.)

В целом: сохраняйте i--/ i++и ++i/ или i++отличия от собеседований при приеме на работу, придерживайтесь array.forEachили выполняйте другие сложные библиотечные функции, если они доступны. ;)


18
Ключевое слово «может быть». Ваш развернутый цикл также может быть медленнее, чем оригинальный. При оптимизации всегда измеряйте, чтобы точно знать, какое влияние оказали ваши изменения.
Джалф

1
@ Правда правда, +1. Разные длины неопущенных (> = 1) отличаются по эффективности. Вот почему удобнее оставлять эту работу библиотекам, если это возможно, не говоря уже о том, что браузеры работают на разных архитектурах, поэтому может быть лучше, если они решат, как это сделать array.each(...). Я не думаю, что они попытались бы поэкспериментировать с открывающимися петлями for.
Барни

1
@BarnabasSzabolcs Вопрос касается именно JavaScript, а не C или других языков. В JS есть есть разница, пожалуйста увидеть мой ответ ниже. Хотя это не относится к вопросу, +1 хороший ответ!
AMK

1
И вот вы идете - +1чтобы получить Great Answerзнак. Действительно отличный ответ. :)
Рохит Джайн

1
APL / A ++ также не является языком. Люди используют их, чтобы выразить, что их интересует не языковая специфика, а то, что эти языки разделяют.
Барни

33

i-- так быстро, как i++

Этот код ниже так же быстро, как ваш, но использует дополнительную переменную:

var up = Things.length;
for (var i = 0; i < up; i++) {
    Things[i]
};

Рекомендуется НЕ оценивать размер массива каждый раз. Для больших массивов можно увидеть снижение производительности.


6
Вы явно не правы здесь. Теперь для управления циклом требуется дополнительная внутренняя переменная (i и выше), и в зависимости от механизма JavaScript это может ограничить потенциал для оптимизации кода. JIT преобразует простые циклы в прямые машинные коды операций, но если в циклах слишком много переменных, JIT не сможет оптимизировать так хорошо. Как правило, оно ограничено архитектурой или регистрами процессора, которые использует JIT. Инициализируйте один раз и спустите просто самое чистое решение, и поэтому оно рекомендуется повсеместно.
Павел П

Дополнительная переменная будет исключена оптимизатором общего интерпретатора / компилятора. Дело в том, чтобы «сравнить с 0» - см мой ответ для дальнейшего объяснения.
Х.-Дирк Шмитт

1
Лучше поставить «вверх» внутри цикла:for (var i=0, up=Things.length; i<up; i++) {}
thdoan

22

Поскольку вас интересует эта тема, взгляните на статью в блоге Грега Реймера о тесте цикла JavaScript. Какой самый быстрый способ кодирования цикла в JavaScript? :

Я создал набор тестов для тестирования циклов для различных способов кодирования циклов в JavaScript. Уже есть несколько таких, но я не нашел ни одного, который признал бы разницу между нативными массивами и коллекциями HTML.

Вы также можете выполнить тест производительности цикла , открыв https://blogs.oracle.com/greimer/resource/loop-test.html(не работает, если JavaScript заблокирован в браузере, например, с помощью NoScript ).

РЕДАКТИРОВАТЬ:

Более поздний тест, созданный Миланом Адамовским, можно выполнить здесь во время выполнения для разных браузеров.

Для тестирования в Firefox 17.0 на Mac OS X 10.6 я получил следующий цикл:

var i, result = 0;
for (i = steps - 1; i; i--) {
  result += i;
}

как самый быстрый предшествует:

var result = 0;
for (var i = steps - 1; i >= 0; i--) {
  result += i;
}

Контрольный пример.


5
Этому посту сейчас 4 года. Учитывая скорость (ха!) Обновления движков JavaScript, большинство рекомендаций, скорее всего, устареет - в статье даже не упоминается Chrome, а это блестящий движок V8 JavaScript.
И Цзян

Да, я скучаю по этой детали, спасибо за указание на это. Событие в то время не было большой разницы между --i или i ++ в цикле for.
Dreamcrash

19

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

a++ < length

на самом деле составляется как

a++
test (a-length)

Таким образом, это занимает больше времени на процессоре при компиляции.


15

Короткий ответ

Для нормального кода, особенно в языке высокого уровня, таком как JavaScript , нет разницы в производительности i++и i--.

Критериями производительности является использование в forцикле и оператор сравнения .

Это относится ко всем языкам высокого уровня и в основном не зависит от использования JavaScript. Объяснение - полученный код ассемблера в нижней строке.

Детальное объяснение

Разница в производительности может возникнуть в цикле. Предпосылкой является то, что на уровне кода ассемблера вы можете видеть, что это compare with 0всего лишь один оператор, который не нуждается в дополнительном регистре.

Это сравнение выполняется на каждом проходе цикла и может привести к ощутимому улучшению производительности.

for(var i = array.length; i--; )

будет оцениваться как псевдокод :

 i=array.length
 :LOOP_START
 decrement i
 if [ i = 0 ] goto :LOOP_END
 ... BODY_CODE
 :LOOP_END

Обратите внимание, что 0 является литералом или, другими словами, постоянным значением.

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

будет оцениваться в псевдокод, подобный этому (предполагается нормальная оптимизация интерпретатора):

 end=array.length
 i=0
 :LOOP_START
 if [ i < end ] goto :LOOP_END
 increment i
 ... BODY_CODE
 :LOOP_END

Обратите внимание, что end является переменной, которая нуждается в регистре процессора . Это может вызвать дополнительный обмен регистров в коде и требует более дорогого оператора сравнения в ifоператоре.

Только мои 5 центов

Для языка высокого уровня удобочитаемость, которая облегчает ремонтопригодность, более важна как незначительное улучшение производительности.

Обычно классическая итерация от начала до конца массива лучше.

Более быстрая итерация от конца массива к началу приводит к возможно нежелательной обратной последовательности.

Пост скриптум

Как указано в комментарии: Разница --iи i--в оценке iдо или после уменьшения.

Лучшее объяснение - попробовать ;-) Вот пример Bash .

 % i=10; echo "$((--i)) --> $i"
 9 --> 9
 % i=10; echo "$((i--)) --> $i"
 10 --> 9

1
1+ Хорошее объяснение. Просто вопрос немного выходит за рамки, не могли бы вы объяснить разницу между ними, --iа i--также?
Афшин Мехрабани

14

Я видел ту же рекомендацию в Sublime Text 2.

Как уже было сказано, главное улучшение - это не оценка длины массива на каждой итерации цикла for. Это хорошо известный метод оптимизации и особенно эффективный в JavaScript, когда массив является частью HTML-документа (выполняется forдля всех liэлементов).

Например,

for (var i = 0; i < document.getElementsByTagName('li').length; i++)

намного медленнее чем

for (var i = 0, len = document.getElementsByTagName('li').length; i < len; i++)

С того места, где я стою, главное улучшение формы в вашем вопросе заключается в том, что она не объявляет дополнительную переменную ( lenв моем примере)

Но если вы спросите меня, все дело не в оптимизации i++против i--, а в том, что вам не нужно оценивать длину массива на каждой итерации (вы можете увидеть тест производительности на jsperf ).


1
Я должен сделать исключение из слова вычисления здесь. Смотрите мой комментарий к ответу Павла . Спецификация ECMA гласит, что длина массива не рассчитывается при обращении к нему.
Кодзиро

Будет ли «оценка» лучшим выбором? Интересный факт, кстати, я не знал, что
BBog

Дополнительная переменная будет исключена оптимизатором общего интерпретатора / компилятора. То же самое относится и к оценке array.length. Дело в том, чтобы «сравнить с 0» - см мой ответ для дальнейшего объяснения.
Х.-Дирк Шмитт

@ H.-DirkSchmitt кроме вашего здравого смысла, долгое время в истории JavaScript компилятор не оптимизировал затраты производительности цепочки поиска. AFAIK V8 был первым, кто попробовал.
Кодзиро

@ H.-DirkSchmitt, как сказал Кодзиро, это хорошо известный и хорошо зарекомендовавший себя трюк. Даже если это больше не актуально в современных браузерах, это все равно не делает его устаревшим. Кроме того, делать это либо с введением новой переменной для длины, либо с помощью хитрости в вопросе OP, это по-прежнему лучший способ, imo. Это просто умная вещь и хорошая практика, я не думаю, что это имеет какое-либо отношение к тому факту, что компилятор был оптимизирован для того, чтобы позаботиться о том, что часто делается неправильно в JS
BBog

13

Я не думаю, что имеет смысл говорить, что i--это быстрее, чем i++в JavaScript.

Прежде всего , это полностью зависит от реализации движка JavaScript.

Во-вторых , при условии, что простейшие конструкции с JIT'ом и переведены в нативные инструкции, тогда i++vs i--будет полностью зависеть от процессора, который его выполняет. То есть на ARM (мобильных телефонах) быстрее снижаться до 0, поскольку уменьшение и сравнение с нулем выполняются в одной инструкции.

Возможно, вы подумали, что один из них был напраснее другого, потому что предложенный способ

for(var i = array.length; i--; )

но предложенный способ не потому, что один быстрее, чем другой, а просто потому, что если вы пишете

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

затем на каждой итерации array.lengthнужно было оценить (возможно, более умный движок JavaScript мог выяснить, что цикл не изменит длину массива). Хотя это выглядит как простое утверждение, на самом деле это какая-то функция, которая вызывается скрытно движком JavaScript.

Другая причина, почему это i--можно считать «более быстрым», заключается в том, что движку JavaScript нужно выделить только одну внутреннюю переменную для управления циклом (переменная для var i). Если сравнивать с массивом array.length или какой-либо другой переменной, тогда для управления циклом требовалось более одной внутренней переменной, а количество внутренних переменных является ограниченным преимуществом движка JavaScript. Чем меньше переменных используется в цикле, тем больше у JIT шансов на оптимизацию. Вот почему i--можно считать быстрее ...


3
Вероятно, стоит тщательно сформулировать, как array.lengthоценивается. Длина не рассчитывается, когда вы ссылаетесь на нее. (Это просто свойство, которое устанавливается при создании или изменении индекса массива ). При наличии дополнительных затрат это происходит потому, что механизм JS не оптимизировал цепочку поиска для этого имени.
Кодзиро

Ну, не знаю, что говорит спецификация Ecma, но, зная о некоторых внутренних элементах различных движков JavaScript, это не просто, getLength(){return m_length; }потому что в этом участвует некоторая домашняя работа. Но если вы попытаетесь думать задом наперед: было бы довольно изобретательно написать реализацию массива, в которой нужно было бы рассчитать длину :)
Павел П

спецификация ECMA требует , чтобы свойство length уже было вычислено. Он lengthдолжен обновляться немедленно при добавлении или изменении индекса свойства этого массива.
Кодзиро

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

1
Х86 как ARM в этом отношении. dec/jnzпротив inc eax / cmp eax, edx / jne.
Питер Кордес

13

Поскольку ни один из других ответов, кажется, не отвечает на ваш конкретный вопрос (более половины из них показывают примеры C и обсуждают языки более низкого уровня, ваш вопрос касается JavaScript), я решил написать свой собственный.

Итак, поехали:

Простой ответ: i-- как правило, быстрее, потому что он не должен запускать сравнение с 0 при каждом запуске, результаты теста для различных методов приведены ниже:

Результаты теста: как доказал этот jsPerf, arr.pop()на самом деле это самый быстрый цикл. Но, сосредоточив внимание на --i, i--, i++и , ++iкак вы просили в вашем вопросе, вот JSPerf (они от множества JSPerf, смотрите , пожалуйста , источники ниже) ; результаты суммированы:

--iи i--то же самое в Firefox, пока i--быстрее в Chrome.

В Chrome основного цикла ( for (var i = 0; i < arr.length; i++)) быстрее , чем i--и в --iто время как в Firefox это медленнее.

И в Chrome, и в Firefox кэширование arr.lengthзначительно быстрее, а Chrome опережает его примерно на 170 000 операций в секунду.

Без существенной разницы, ++iбыстрее, чем i++в большинстве браузеров, AFAIK, это никогда не бывает наоборот в любом браузере.

Короткое резюме: arr.pop() самая быстрая петля на сегодняшний день; для специально упомянутых циклов, i--это самый быстрый цикл.

Источники: http://jsperf.com/fastest-array-loops-in-javascript/15 , http://jsperf.com/ipp-vs-ppi-2

Надеюсь, это ответит на ваш вопрос.


1
Кажется, что ваш popтест кажется очень быстрым, потому что он уменьшает размер массива до 0 для большей части цикла - насколько я могу судить. Однако, чтобы убедиться, что все справедливо, я разработал этот jsperf для создания массива одинаковым образом с каждым тестом - который, похоже, .shift()является победителем для моих немногих браузеров - не то, чего я ожидал :) :) jsperf.com / сравнить разные типы циклов
Pebbl

Голосовали за то, что упомянули только одно ++i: D
jaggedsoft

12

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

В некоторых случаях доступ к членам массива в порядке столбцов происходит быстрее, чем в строке, из-за увеличения коэффициента попадания.


1
Если бы только ОП спросил, почему прохождение одной и той же матрицы в разных ордерах может занять разное время ...
dcow

1
Учитывая, что в операционных системах управление их памятью основано на пейджинге, когда процессу требуются данные, отсутствующие на кэшированных страницах, возникает сбой страницы в ОС, и он должен перенести целевую страницу в кэш ЦП и заменить ее на другую страницу, поэтому вызывает накладные расходы при обработке, которая занимает больше времени, чем когда целевая страница находится в кэше процессора. Предположим, что мы определили большой массив, что каждая строка в нем больше, чем размер ОС страницы, и мы обращаемся к нему в порядке строк, в этом случае коэффициент ошибок страниц увеличивается, и результат медленнее, чем доступ к массиву в порядке столбцов.
Акбар Баяти

Сбои страниц - это не то же самое, что ошибки кеша. Вы только страницу ошибки, если ваша память была выгружена на диск. Доступ к многомерным массивам в последовательном порядке их хранения в памяти происходит быстрее из-за локальности кэша (с использованием всех байтов строки кэша при его извлечении), а не из-за сбоев страниц. (если ваш рабочий набор не слишком велик для используемого вами компьютера.)
Питер Кордес

10

В последний раз я беспокоился об этом, когда писал сборку 6502 (8-битная, да!). Большим преимуществом является то, что большинство арифметических операций (особенно декременты) обновляли набор флагов, одним из которых был Zиндикатор «достигнут нуля».

Итак, в конце цикла вы просто выполнили две инструкции: DEC(уменьшение) и JNZ(переход, если не ноль), сравнение не требуется!


В случае с JavaScript, очевидно, это не применяется (поскольку он работает на процессорах, у которых нет таких кодов операций). Скорее всего, настоящая причина i--vs i++заключается в том, что с первым вы не вводите дополнительные переменные управления в область действия цикла. Смотрите мой ответ ниже ...
Павел П

1
да, это действительно не относится; но это очень распространенный стиль C, и он выглядит чище для тех из нас, кто привык к этому. :-)
Хавьер

В этом отношении x86 и ARM похожи на 6502. dec/jnzа не inc/cmp/jneдля случая x86. Вы не увидите, что пустой цикл будет работать быстрее (оба будут насыщать пропускную способность ветки), но обратный отсчет немного уменьшает издержки цикла. Текущие устройства предварительной выборки Intel HW также не беспокоятся о шаблонах доступа к памяти, которые идут в порядке убывания. Старые процессоры, я думаю, могут отслеживать как 4 обратных потока и 6 или 10 прямых потоков, IIRC.
Питер Кордес

7

Это можно объяснить тем, что JavaScript (и все языки) в конечном итоге превращаются в коды операций для работы на процессоре. Процессоры всегда имеют одну инструкцию для сравнения с нулем, что чертовски быстро.

Кроме того, если вы можете гарантировать countвсегда >= 0, вы можете упростить:

for (var i = count; i--;)
{
  // whatever
}

1
Более короткий исходный код не обязательно означает, что он будет быстрее. Вы измерили это?
Грег Хьюгилл

Упс я пропустил этот. Гвоздь на голову там, гл.
Роберт

1
Я хотел бы видеть источники сборки, где сравнение с 0 отличается. Прошло несколько лет, но в свое время я написал тонну ассемблерного кода, и я не могу придумать способ сделать сравнение / тестирование с 0 таким способом, который не может быть одинаково быстрым. для любого другого целого числа. Тем не менее, то, что вы говорите, звучит правдоподобно. Разочарован тем, что не могу понять почему!
Брайан Ноблаух

7
@Brian Knoblauch: Если вы используете такую ​​инструкцию, как «dec eax» (код x86), тогда эта инструкция автоматически устанавливает флаг Z (ноль), который вы можете сразу проверить без необходимости использования другой инструкции сравнения между ними.
Грег Хьюгилл

1
Когда я интерпретирую Javascript, я сомневаюсь, что коды операций будут узким местом. Скорее всего, меньше токенов означает, что интерпретатор сможет быстрее обрабатывать исходный код.
Тодд Оуэн

6

Это не зависит от знака --или ++, но зависит от условий, которые вы применяете в цикле.

Например: ваш цикл будет быстрее, если переменная имеет статическое значение, чем если бы ваш цикл проверял условия каждый раз, например длину массива или другие условия.

Но не беспокойтесь об этой оптимизации, потому что на этот раз ее эффект измеряется в наносекундах.


циклы с несколькими наносекундами могут стать секундами ... оптимизировать, когда у вас есть на это время, - плохая идея
Buksy

6

for(var i = array.length; i--; )не намного быстрее. Но когда вы заменяете array.lengthна super_puper_function(), это может быть значительно быстрее (так как он вызывается на каждой итерации). В этом разница.

Если вы собираетесь изменить его в 2014 году, вам не нужно думать об оптимизации. Если вы собираетесь изменить его с помощью функции «Поиск и замена», вам не нужно думать об оптимизации. Если у вас нет времени, вам не нужно думать об оптимизации. Но теперь у вас есть время подумать об этом.

PS: i--не быстрее чем i++.


6

Короче говоря: в JavaScript нет абсолютно никакой разницы.

Прежде всего, вы можете проверить это самостоятельно:

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

Итак, насколько вы можете видеть, нет никакой разницы между производительностью в любой среде.

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

  1. Сделайте var a = array.length;заявление, чтобы вы не вычисляли его значение каждый раз в цикле
  2. Сделайте раскрутку цикла http://en.wikipedia.org/wiki/Loop_unwinding

Но вы должны понимать, что улучшение, которое вы можете получить, будет настолько незначительным, что в большинстве случаев вы даже не будете беспокоиться об этом.

Мое собственное мнение, почему появилось такое заблуждение (Dec против Inc)

Давным-давно была общая машинная инструкция, DSZ (Decrement и Skip on Zero). Люди, которые программировали на ассемблере, использовали эту инструкцию для реализации циклов, чтобы сохранить регистр. Теперь эти древние факты устарели, и я уверен, что вы не получите никакого улучшения производительности ни на одном языке, используя это псевдо улучшение.

Я думаю, что единственный способ распространения таких знаний в наше время - это когда вы читаете чужой код. Посмотрите на такую ​​конструкцию и спросите, почему она была реализована, и вот ответ: «это улучшает производительность, потому что она сравнивается с нулем». Вы растерялись из-за высшего знания своего коллеги и думаете использовать его, чтобы быть намного умнее :-)


Интересно, что для ваших тестов, работающих под Firefox 16.0.2 на win7, цикл декремента был на 29% медленнее ... РЕДАКТИРОВАТЬ: Не обращайте внимания. Повторные тесты оказались неубедительными, в моих результатах тестов наблюдается удивительное количество шума для последующих прогонов. Не совсем уверен, почему.

Да, я пытался объяснить это, закрыв все остальное и просто запустив тесты. Все еще получил удивительные результаты. Очень странно.

Я думаю, что вы упустили реальную мысль, почему переход к нулю считается лучшим в JavaScript. В основном потому, что таким образом только одна переменная контролирует выполнение цикла, например, оптимизатор / JITer имеет больше возможностей для улучшения. Использование array.lengthне обязательно влечет за собой снижение производительности, просто потому, что виртуальная машина JS достаточно умна, чтобы выяснить, не был ли массив изменен телом цикла. Смотрите мой ответ ниже.
Павел П

1
придирки: этот древний факт (оптимизация на ассемблере) не устарел, просто загадочен. как вам не нужно знать, если вы действительно не знаете. :-)
Хавьер

6

Я сделал сравнение на jsbench .

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

for ( var i = 1; i <= array.length; i++ )

Вы оцениваете .lengthкаждый раз, когда увеличиваете i. В этом:

for ( var i = 1, l = array.length; i <= l; i++ )

Вы оцениваете .lengthтолько один раз, когда заявляете i. В этом:

for ( var i = array.length; i--; )

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

Цикл с вызовом функции (определяется в другом месте):

for (i = values.length; i-- ;) {
  add( values[i] );
}

Цикл с встроенным кодом:

var sum = 0;
for ( i = values.length; i-- ;) {
  sum += values[i];
}

Если вы можете встроить свой код вместо вызова функции, не жертвуя удобочитаемостью, вы можете сделать цикл на порядок быстрее!


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

  1. Узкое место может быть в другом месте (ajax, reflow, ...)
  2. Вы можете выбрать лучший алгоритм
  3. Вы можете выбрать лучшую структуру данных

Но помните:

Код написан для людей, которые могут читать, и только для машин.


+1 за этот ответ и за тест. Я добавил тесты forEach и переделал эталонный тест в отдельный файл, работающий как в браузере, так и в Node. jsfiddle.net/hta69may/2 . Для узла «Обратный цикл, неявное сравнение, встроенный код» самый быстрый. Но тестирование в FF 50 показало любопытные результаты: не только время было почти в 10 раз меньше (!), Но и оба теста «forEach» были такими же быстрыми, как и «обратный цикл». Может, ребята из Node должны использовать движок Mozilla JS вместо V8? :)
Fr0sT

4

То, как вы делаете это сейчас, не быстрее (кроме того, что это неопределенный цикл, я думаю, вы хотели это сделать i--.

Если вы хотите сделать это быстрее, сделайте:

for (i = 10; i--;) {
    //super fast loop
}

Конечно, вы не заметите это на такой маленькой петле. Причина в том, что он быстрее, потому что вы уменьшаете значение i, проверяя, что оно «true» (оно оценивается как «false», когда достигает 0)


1
Вам не хватает точки с запятой? (i = 10; i--;)
Searlea

Да, да, я исправил ошибку. И если мы собираемся быть разборчивыми, я укажу, что вы забыли свою точку с запятой после своего я! Хех.
djdd87

Почему это будет быстрее? Более короткий источник не означает, что это будет быстрее. Вы измерили это?
Грег Хьюгилл

4
Да, я измерил это, и я не говорю, что более короткий источник делает это быстрее, но меньшее количество операций делает это быстрее.
peirix

4

Иногда внесение очень незначительных изменений в способ написания нашего кода может существенно повлиять на скорость выполнения нашего кода. Одна область, где незначительное изменение кода может иметь большое значение для времени выполнения, это где у нас есть цикл for, который обрабатывает массив. Если массив состоит из элементов на веб-странице (например, переключателей), изменение имеет наибольший эффект, но все же стоит применить это изменение, даже если массив является внутренним по отношению к коду Javascript.

Обычный способ кодирования цикла for для обработки массива lis выглядит следующим образом:

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

Проблема в том, что для оценки длины массива с использованием myArray.length требуется время, а способ, которым мы закодировали цикл, означает, что эта оценка должна выполняться каждый раз вокруг цикла. Если массив содержит 1000 элементов, то длина массива будет оценена 1001 раз. Если бы мы смотрели на переключатели и имели myForm.myButtons.length, то это займет еще больше времени для оценки, так как соответствующая группа кнопок в указанной форме должна сначала быть найдена, прежде чем длина может быть оценена каждый раз вокруг цикла.

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

Что мы можем сделать, чтобы исправить это для цикла, в котором размер фиксирован, так это вычислить длину один раз в начале цикла и сохранить ее в переменной. Затем мы можем проверить переменную, чтобы решить, когда завершить цикл. Это намного быстрее, чем вычисление длины массива каждый раз, особенно когда массив содержит больше, чем несколько записей или является частью веб-страницы.

Код для этого:

for (var i = 0, var j = myArray.length; i < j; i++) {...

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

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

Последний код, который обрабатывает наш массив наиболее эффективным способом:

for (var i = myArray.length-1; i > -1; i--) {...

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


4

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

Люди, которые говорят об использовании --(или ++) для эффективного использования инструкций по сборке, ошибаются. Эти инструкции применяются к целочисленной арифметике, и в JavaScript нет целых чисел, только числа .

Вы должны написать читабельный код.


3

Во многих случаях это, по сути, не имеет ничего общего с тем, что процессоры могут сравниваться с нулем быстрее, чем другие сравнения.

Это потому, что только несколько движков Javascript (те, что в списке JIT) фактически генерируют код машинного языка.

Большинство движков Javascript создают внутреннее представление исходного кода, которое они затем интерпретируют (чтобы понять, на что это похоже, посмотрите в нижней части этой страницы на SpiderMonkey в Firefox ). Обычно, если фрагмент кода делает практически то же самое, но приводит к более простому внутреннему представлению, он будет работать быстрее.

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


3

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

В низкоуровневой сборке есть инструкция зацикливания, которая называется DJNZ (уменьшить и перейти, если не ноль). Таким образом, декремент и переход - все в одной инструкции, что делает его, возможно, немного быстрее, чем INC и JL / JB (увеличение, переход, если меньше, чем / прыжок, если ниже). Кроме того, сравнение с нулем проще, чем сравнение с другим числом. Но все это действительно маргинально, а также зависит от целевой архитектуры (может иметь значение, например, от Arm в смартфоне).

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


Для записи DJNZесть инструкция в ISA 8051 (z80). x86 имеет dec/jnzвместо inc/cmp/jne, и, видимо, рука имеет нечто подобное. Я уверен, что это не является причиной различий в Javascript; это от того, чтобы иметь больше, чтобы оценить в состоянии цикла.
Питер Кордес

3

Он используется , чтобы сказать , что --i был быстрее (в C ++) , потому что есть только один результат, уменьшенное значение. i-- необходимо сохранить уменьшенное значение обратно в i, а также сохранить исходное значение в качестве результата (j = i--;). В большинстве компиляторов это использовало два регистра, а не один, что могло привести к тому, что другая переменная должна была быть записана в память, а не сохранена как переменная регистра.

Я согласен с теми, кто сказал, что в эти дни нет никакой разницы.


3

Очень простыми словами

«i-- и i ++. На самом деле, они оба занимают одно и то же время».

но в этом случае, когда у вас есть инкрементная операция .. процессор оценивает .length каждый раз, когда переменная увеличивается на 1, а в случае уменьшения .. особенно в этом случае, он будет оценивать .length только один раз, пока мы не получим 0.


3

Во-первых, i++и i--занимайте ровно столько же времени на любом языке программирования, включая JavaScript.

Следующий код занимает много разного времени.

Быстро:

for (var i = 0, len = Things.length - 1; i <= len; i++) { Things[i] };

Медленный:

for (var i = 0; i <= Things.length - 1; i++) { Things[i] };

Поэтому следующий код тоже занимает другое время.

Быстро:

for (var i = Things.length - 1; i >= 0; i--) { Things[i] };

Медленный:

for (var i = 0; i <= Things.length - 1; i++) { Things[i] };

PS Slow является медленным только для нескольких языков (движки JavaScript) из-за оптимизации компилятора. Лучший способ - использовать «<» вместо «<=» (или «=») и «--i» вместо «i--» .


3

Не много времени тратится на i- или i ++. Если вы углубитесь в архитектуру ЦП, то она ++будет более быстрой, чем операция --, поскольку --операция будет дополнять 2, но происходит внутри аппаратного обеспечения, так что это будет быстрым, и между этими операциями не будет существенной разницы, ++а --также эти операции считаются Наименьшее время, затрачиваемое на процессор.

Цикл for работает так:

  • Инициализируйте переменную один раз в начале.
  • Проверьте ограничение во втором операнде петли, <, >, <=и т.д.
  • Затем примените цикл.
  • Увеличивайте цикл и снова запускайте эти процессы.

Так,

for (var i = Things.length - 1; i >= 0; i--) {
    Things[i]
}; 

вычислит длину массива только один раз в начале, и это не много времени, но

for(var i = array.length; i--; ) 

рассчитает длину в каждом цикле, так что это займет много времени.


var i = Things.length - 1; i >= 0; i--рассчитаем длину 1 раз тоже.
Дмитрий

1
Я не уверен, что --означает « операция будет дополнять 2», но я предполагаю, что это означает, что она что-то отрицает. Нет, это не отрицает ничего на любой архитектуре. Вычитать 1 так же просто, как сложить 1, вы просто создаете схему, которая заимствует, а не несет.
Олат

1

Лучший подход к ответу на подобные вопросы - это попробовать. Установите цикл, который насчитывает миллион итераций или что-то еще, и делайте это в обоих направлениях. Время оба цикла, и сравнить результаты.

Ответ, вероятно, будет зависеть от того, какой браузер вы используете. Некоторые из них будут иметь другие результаты, чем другие.


4
Это все еще не ответило бы на его вопрос о том, почему это быстрее. Он просто получит эталонный тест, не зная, почему это так ...
peirix

3
Это правда. Однако, не зная точной реализации каждого движка Javascript в каждом браузере, практически невозможно ответить на вопрос «почему». Многие ответы здесь включают в себя отдельные рекомендации, такие как «использовать предопределение вместо постдекремента» (трюк оптимизации C ++) и «сравнивать с нулем», что может быть правдой в языках компиляции с нативным кодом, но Javascript довольно далек от чистого металла ЦПУ.
Грег Хьюгилл

4
-1 Я совершенно не согласен с тем, что лучший способ - это попробовать. Пара примеров не заменяет коллективных знаний, и в этом весь смысл форумов, подобных этому.
Кристоф

1

Люблю это, много оценок, но нет ответа: D

Проще говоря сравнение с нулем всегда самое быстрое сравнение

Так что (a == 0) на самом деле быстрее возвращает True, чем (a == 5)

Он маленький и незначительный, и из 100 миллионов строк в коллекции его можно измерить.

то есть в цикле вы могли бы сказать, где я <= array.length и увеличивая я

в нисходящем цикле вы можете говорить, где i> = 0, и вместо этого уменьшать i.

Сравнение происходит быстрее. Не «направление» петли.


На указанный вопрос нет ответа, поскольку все движки Javascript разные, и ответ зависит от того, в каком именно браузере вы его измеряете.
Грег Хьюгилл

Нет, это принципиально приемлемые сравнения с нулем самые быстрые. Хотя ваши утверждения также верны, золотое правило сравнения с нулем является абсолютным.
Роберт

4
Это верно только в том случае, если компилятор решит выполнить эту оптимизацию, что, безусловно, не гарантируется. Компилятор может генерировать точно такой же код для (a == 0) и (a == 5), за исключением значения константы. ЦП не будет сравнивать с 0 быстрее, чем с любым другим значением, если обе стороны сравнения являются переменными (с точки зрения ЦП). Обычно только компиляторы нативного кода имеют возможность оптимизировать на этом уровне.
Грег Хьюгилл

1

ПОМОГИТЕ ДРУГИМ ИЗБЕГАТЬ ГОЛОВНОЙ БУКВЫ --- ПРОЧИТАЙТЕ ЭТО!

Самый популярный ответ на этой странице не работает для Firefox 14 и не проходит через jsLinter. Для циклов while требуется оператор сравнения, а не присваивание. Это работает на хром, сафари и даже т.е. Но умирает в Firefox.

СЛОМАНО!

var i = arr.length; //or 10
while(i--)
{
  //...
}

ЭТО БУДЕТ РАБОТАТЬ! (работает на firefox, передает jsLinter)

var i = arr.length; //or 10
while(i>-1)
{
  //...
  i = i - 1;
}

2
Я только что попробовал это на Firefox 14, и он работал нормально. Я видел примеры из производственных библиотек, которые используют while (i--)и которые работают во многих браузерах. Может ли быть что-то странное в вашем тесте? Вы использовали бета-версию Firefox 14?
Мэтт Браун

1

Это всего лишь предположение, но, возможно, это потому, что процессору проще сравнивать что-либо с 0 (i> = 0), а не с другим значением (i <Things.length).


3
+1 Не проголосовали другие. Хотя повторная оценка длины является гораздо большей проблемой, чем фактическое увеличение / уменьшение, проверка конца цикла может быть важной. Мой компилятор C выдает несколько предупреждений о циклах, таких как: «замечание # 1544-D: (ULP 13.1) Подсчет обнаруженных циклов. Рекомендуйте обратный отсчет циклов, поскольку обнаружение нулей проще»
harper
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.