Наш учитель информатики однажды сказал, что по некоторым причинам более эффективно считать в обратном порядке, чем подсчитать. Например, если необходимо использовать ДЛЯ цикла, и индекс цикла не используется где-нибудь (как печать строки N * на экран), я подразумеваю что код как это:
for (i = N; i >= 0; i--)
putchar('*');
лучше, чем:
for (i = 0; i < N; i++)
putchar('*');
Это действительно верно? И если так, кто-либо знает почему?
Это правда? и если да, то кто-нибудь знает, почему?
В древние времена, когда компьютеры еще вручную изготавливали из плавленого кварца, когда 8-битные микроконтроллеры бродили по Земле и когда ваш учитель был молод (или учитель вашего учителя был молод) , была обычная машинная инструкция, называемая декрементировать и пропускать, если ноль (DSZ). Программисты горячей сборки использовали эту инструкцию для реализации циклов. Более поздние машины получили более изящные инструкции, но все еще оставалось довольно много процессоров, на которых было дешевле сравнивать что-то с нулем, чем сравнивать с чем-либо еще. (Это верно даже для некоторых современных RISC-машин, таких как PPC или SPARC, которые резервируют весь регистр, чтобы он всегда был равен нулю.)
Итак, если вы настраиваете свои циклы для сравнения с нулем вместо N
, что может случиться?
Приведут ли эти различия к какому-либо измеримому улучшению реальных программ на современном вышедшем из строя процессоре? Очень маловероятно. На самом деле, я был бы впечатлен, если бы вы смогли показать ощутимое улучшение даже на микробенчмарке.
Резюме: Я ударил вашего учителя по голове! Вы не должны изучать устаревшие псевдо-факты о том, как организовать циклы.Вы должны понимать, что самая важная вещь в циклах - это убедиться, что они завершают , дают правильных ответов и легко читаются . Я бы хотел, чтобы ваш учитель сосредоточился на важных вещах, а не на мифологии.
Обратный отсчет быстрее, чем вверх?
Может быть. Но в более чем 99% случаев это не имеет значения, поэтому вы должны использовать наиболее `` разумный '' тест для завершения цикла, и под разумным я подразумеваю, что читателю требуется минимум размышлений, чтобы выяснить что делает цикл (включая то, что заставляет его останавливаться). Сделайте так, чтобы ваш код соответствовал мысленной (или документированной) модели того, что он делает.
Если цикл работает вверх через массив (или список, или что-то еще), увеличивающийся счетчик часто будет лучше соответствовать тому, как читатель может думать о том, что делает цикл - закодируйте свой цикл таким образом.
Но если вы работаете с контейнером, в котором есть N
элементов, и удаляете их по ходу работы, возможно, будет разумнее снизить счетчик.
Немного подробнее о «возможно» в ответе:
Это правда, что на большинстве архитектур для проверки вычисления, приводящего к нулю (или переходу от нуля к отрицательному), не требуется явных инструкций тестирования - результат может проверять напрямую.Если вы хотите проверить, дает ли результат вычисления какое-либо другое число, поток инструкций обычно должен иметь явную инструкцию для проверки этого значения. Однако, особенно с современными процессорами, этот тест обычно добавляет меньше времени, чем уровень шума, к циклической конструкции. В частности, если этот цикл выполняет ввод-вывод.
С другой стороны, если вы отсчитываете от нуля и используете счетчик в качестве индекса массива, например, вы можете обнаружить, что код работает против архитектуры памяти системы - чтение из памяти часто приводит к « смотреть вперед 'на несколько ячеек памяти после текущего в ожидании последовательного чтения. Если вы работаете в обратном направлении через память, система кэширования может не ожидать чтения из области памяти по более низкому адресу памяти. В этом случае возможно, что зацикливание «в обратном направлении» может снизить производительность. Тем не менее, я бы все равно закодировал цикл таким образом (если производительность не стала проблемой), потому что правильность имеет первостепенное значение, а приведение кода в соответствие с моделью - отличный способ обеспечить правильность. Неправильный код настолько неоптимизирован, насколько это возможно.
Так что я бы склонен забыть совет профессора (конечно, не о его тесте - вы все равно должны быть прагматичными в классе), если и до тех пор, пока производительность кода действительно не будет иметь значения.
Нет, это не совсем так. Одна из ситуаций, когда это может быть быстрее, это когда в противном случае вы бы вызывали функцию для проверки границ во время каждой итерации цикла.
for(int i=myCollection.size(); i >= 0; i--)
{
...
}
Но если так делать менее наглядно, то смысла в этом нет. В современных языках в любом случае следует использовать цикл foreach, когда это возможно. Вы специально упомянули случай, когда следует использовать цикл foreach - когда вам не нужен индекс.
Дело в том, что при обратном отсчете вам не нужно проверять i> = 0
отдельно для уменьшения i
. Обратите внимание:
for (i = 5; i--;) {
alert(i); // alert boxes showing 4, 3, 2, 1, 0
}
И сравнение, и уменьшение i
могут быть выполнены в одном выражении.
См. Другие ответы, почему это сводится к меньшему количеству инструкций x86.
Что касается того, имеет ли это значение для вашего приложения, я полагаю, это зависит от того, сколько у вас циклов и насколько глубоко они вложены. Но для меня это так же легко читать, так что я все равно это делаю.
Это интересный вопрос, но с практической точки зрения я не думаю, что он важен и не делает один цикл лучше другого.
Согласно этой странице википедии: Секунда координации , «... солнечные сутки становятся длиннее на 1,7 мс с каждым столетием, в основном из-за приливного трения». Но если вы считаете дни до своего дня рождения, неужели вас волнует эта крошечная разница во времени?
Более важно, чтобы исходный код был легко читаемым и понятным. Эти два цикла являются хорошим примером того, почему важна удобочитаемость - они не повторяются одинаковое количество раз.
Я готов поспорить, что большинство программистов прочитают (i = 0; i
Как ни странно, похоже, что разница есть. По крайней мере, в PHP. Рассмотрим следующий тест:
<?php
print "<br>".PHP_VERSION;
$iter = 100000000;
$i=$t1=$t2=0;
$t1 = microtime(true);
for($i=0;$i<$iter;$i++){}
$t2 = microtime(true);
print '<br>$i++ : '.($t2-$t1);
$t1 = microtime(true);
for($i=$iter;$i>0;$i--){}
$t2 = microtime(true);
print '<br>$i-- : '.($t2-$t1);
$t1 = microtime(true);
for($i=0;$i<$iter;++$i){}
$t2 = microtime(true);
print '<br>++$i : '.($t2-$t1);
$t1 = microtime(true);
for($i=$iter;$i>0;--$i){}
$t2 = microtime(true);
print '<br>--$i : '.($t2-$t1);
Результаты интересны:
PHP 5.2.13
$i++ : 8.8842368125916
$i-- : 8.1797409057617
++$i : 8.0271911621094
--$i : 7.1027431488037
PHP 5.3.1
$i++ : 8.9625310897827
$i-- : 8.5790238380432
++$i : 5.9647901058197
--$i : 5.4021768569946
Если кто-то знает почему, было бы неплохо узнать:)
РЕДАКТИРОВАТЬ : Результаты такие же, даже если вы начинаете считать не с 0, а с других произвольное значение. Так что, вероятно, разница не только в сравнении с нулем?
На некоторых старых процессорах есть / были такие инструкции, как DJNZ
== "уменьшить и перейти, если не ноль". Это позволяло создавать эффективные циклы, в которых вы загружали начальное значение счетчика в регистр, а затем вы могли эффективно управлять циклом уменьшения с помощью одной инструкции. Мы говорим здесь об ISA 1980-х годов - ваш учитель серьезно оторван, если он думает, что это «практическое правило» все еще применимо к современным процессорам.
Теперь, я думаю, вам хватило лекций по сборке:) Я хотел бы представить вам еще одну причину для подхода top->down.
Причина идти сверху очень проста. В теле цикла вы можете случайно изменить границу, что может привести к некорректному поведению или даже к незавершению цикла.
Посмотрите на эту небольшую часть кода Java (язык не имеет значения, я думаю, по этой причине):
System.out.println("top->down");
int n = 999;
for (int i = n; i >= 0; i--) {
n++;
System.out.println("i = " + i + "\t n = " + n);
}
System.out.println("bottom->up");
n = 1;
for (int i = 0; i < n; i++) {
n++;
System.out.println("i = " + i + "\t n = " + n);
}
Итак, я хочу сказать, что вам следует подумать о том, что предпочтительнее - идти сверху вниз или иметь константу в качестве границы.
Боб,
Нет, пока вы не займетесь микрооптимизацией, и тогда у вас под рукой будет руководство для вашего процессора. Кроме того, если бы вы занимались подобными вещами, вам, вероятно, не нужно было бы задавать этот вопрос :-) Но ваш учитель, очевидно, не разделяет эту идею....
Есть 4 вещи, которые нужно рассмотреть в вашем примере с циклом:
for (i=N;
i>=0; //thing 1
i--) //thing 2
{
putchar('*'); //thing 3
}
Сравнение (как указывали другие) относится к конкретным процессорным архитектурам. Существует больше типов процессоров, чем те, на которых работает Windows. В частности, может существовать инструкция, которая упрощает и ускоряет сравнение с 0.
В некоторых случаях быстрее регулировать вверх или вниз. Как правило, хороший компилятор поймет это и переделает цикл, если сможет. Однако не все компиляторы хороши.
Вы обращаетесь к системному вызову с помощью putchar. Это очень медленно. Кроме того, вы выводите изображение на экран (косвенно). Это еще медленнее. Подумайте о соотношении 1000:1 или больше. В этой ситуации тело цикла полностью и полностью перевешивает стоимость настройки/сравнения цикла.
Кэш и расположение памяти могут сильно влиять на производительность. В данной ситуации это не имеет значения. Однако, если вы обращаетесь к массиву и вам нужна оптимальная производительность, то вам стоило бы изучить, как ваш компилятор и процессор расположили доступы к памяти, и настроить свою программу так, чтобы максимально использовать это. Пример с массивом приведен в связи с умножением матриц.
В C для psudo-assembly:
for (i = 0; i < 10; i++) {
foo(i);
}
превращается в
clear i
top_of_loop:
call foo
increment i
compare 10, i
jump_less top_of_loop
, а:
for (i = 10; i >= 0; i--) {
foo(i);
}
превращается в
load i, 10
top_of_loop:
call foo
decrement i
jump_not_neg top_of_loop
. Обратите внимание на отсутствие сравнения в вторая псудо-сборка. На многих архитектурах есть флаги, которые устанавливаются арифматическими операциями (сложение, вычитание, умножение, деление, увеличение, уменьшение), которые вы можете использовать для переходов. Они часто дают вам то, что по сути является сравнением результата операции с 0 бесплатно. Фактически на многих архитектурах
x = x - 0
семантически совпадает с
compare x, 0
. Кроме того, сравнение с 10 в моем примере может привести к худшему коду. 10, возможно, придется жить в регистре, поэтому, если их не хватает, это стоит и может привести к дополнительному коду для перемещения или перезагрузки 10 каждый раз в цикле.
Компиляторы могут иногда переупорядочивать код, чтобы воспользоваться этим, но это часто бывает сложно, потому что они часто не могут быть уверены, что изменение направления в цикле семантически эквивалентно.
Вот что может произойти на некотором оборудовании в зависимости от того, что компилятор может определить о диапазоне используемых вами чисел: с помощью цикла увеличения вы должны test i
i> = 0
. Это экономит тест на каждый цикл цикла.
В действительности, на современном конвейерном аппаратном обеспечении процессоров этот материал почти наверняка не имеет значения, поскольку не существует простого отображения 1-1 от инструкций к тактовым циклам. (Хотя я мог себе представить, что это произойдет, если вы будете делать такие вещи, как генерация точно синхронизированных видеосигналов с микроконтроллера. Но тогда вы все равно будете писать на языке ассемблера.)
Да!!!
Счет от N до 0 немного быстрее, чем счет от 0 до N в смысле того, как аппаратура будет обрабатывать сравнение...
Обратите внимание на сравнение в каждом цикле
i>=0
i<N
Большинство процессоров имеют инструкцию сравнения с нулем... поэтому первое сравнение будет переведено в машинный код как:
Но во втором случае нужно каждый раз загружать N из памяти
Так что это не из-за отсчета вниз или вверх... А из-за того, как ваш код будет переведен в машинный код...
Поэтому счет от 10 до 100 - это то же самое, что и счет от 100 до 10
.
Но счет от i=100 до 0 быстрее, чем от i=0 до 100 - в большинстве случаев
А счет от i=N до 0 быстрее, чем от i=0 до N
Связанное: Почему n++ выполняется быстрее, чем n=n+1?
независимо от направления всегда используйте префиксную форму (++i вместо i++)!
for (i=N; i>=0; --i)
или
for (i=0; i<N; ++i)
Объяснение: http://www.eskimo.com/~scs/cclass/notes/sx7b.html
Кроме того, вы можете написать
for (i=N; i; --i)
Но я ожидал, что современные компиляторы смогут делать именно такие оптимизации.
Отсчет идет быстрее в таком случае:
for (i = someObject.getAllObjects.size(); i >= 0; i--) {…}
потому что someObject.getAllObjects.size()
выполняется один раз в начале.
Конечно, аналогичного поведения можно добиться, вызывая size()
вне цикла, как упоминал Питер:
size = someObject.getAllObjects.size();
for (i = 0; i < size; i++) {…}
В наборе инструкций Intel x86 построение цикла для обратного отсчета до нуля обычно можно выполнить с меньшим количеством инструкций, чем цикл, который считает до ненулевого условия выхода.В частности, регистр ECX традиционно используется в качестве счетчика циклов в x86 asm, а в наборе инструкций Intel есть специальная инструкция перехода jcxz, которая проверяет регистр ECX на ноль и выполняет переходы на основе результата теста.
Однако разница в производительности будет незначительной, если ваш цикл уже не очень чувствителен к счетчикам тактовых циклов. Обратный отсчет до нуля может сократить 4 или 5 тактовых циклов на каждой итерации цикла по сравнению с обратным отсчетом, так что это скорее новинка, чем полезный метод.
Кроме того, в наши дни хороший оптимизирующий компилятор должен уметь преобразовывать исходный код цикла с обратным отсчетом в машинный код с обратным отсчетом до нуля (в зависимости от того, как вы используете переменную индекса цикла), поэтому действительно нет причин писать ваши петли странными способами, просто чтобы выжать цикл или два здесь и там.