Это быстрее для считания в обратном порядке, чем это должно подсчитать?

Наш учитель информатики однажды сказал, что по некоторым причинам более эффективно считать в обратном порядке, чем подсчитать. Например, если необходимо использовать ДЛЯ цикла, и индекс цикла не используется где-нибудь (как печать строки N * на экран), я подразумеваю что код как это:

for (i = N; i >= 0; i--)  
  putchar('*');  

лучше, чем:

for (i = 0; i < N; i++)  
  putchar('*');  

Это действительно верно? И если так, кто-либо знает почему?

130
задан nicael 21 May 2014 в 18:43
поделиться

15 ответов

Это правда? и если да, то кто-нибудь знает, почему?

В древние времена, когда компьютеры еще вручную изготавливали из плавленого кварца, когда 8-битные микроконтроллеры бродили по Земле и когда ваш учитель был молод (или учитель вашего учителя был молод) , была обычная машинная инструкция, называемая декрементировать и пропускать, если ноль (DSZ). Программисты горячей сборки использовали эту инструкцию для реализации циклов. Более поздние машины получили более изящные инструкции, но все еще оставалось довольно много процессоров, на которых было дешевле сравнивать что-то с нулем, чем сравнивать с чем-либо еще. (Это верно даже для некоторых современных RISC-машин, таких как PPC или SPARC, которые резервируют весь регистр, чтобы он всегда был равен нулю.)

Итак, если вы настраиваете свои циклы для сравнения с нулем вместо N , что может случиться?

  • Вы можете сохранить регистр
  • Вы можете получить инструкцию сравнения с меньшим двоичным кодированием
  • Если предыдущая инструкция устанавливает флаг (вероятно, только на машинах семейства x86), вы можете не даже нужна явная инструкция сравнения

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

Резюме: Я ударил вашего учителя по голове! Вы не должны изучать устаревшие псевдо-факты о том, как организовать циклы.Вы должны понимать, что самая важная вещь в циклах - это убедиться, что они завершают , дают правильных ответов и легко читаются . Я бы хотел, чтобы ваш учитель сосредоточился на важных вещах, а не на мифологии.

369
ответ дан 24 November 2019 в 00:19
поделиться

Обратный отсчет быстрее, чем вверх?

Может быть. Но в более чем 99% случаев это не имеет значения, поэтому вы должны использовать наиболее `` разумный '' тест для завершения цикла, и под разумным я подразумеваю, что читателю требуется минимум размышлений, чтобы выяснить что делает цикл (включая то, что заставляет его останавливаться). Сделайте так, чтобы ваш код соответствовал мысленной (или документированной) модели того, что он делает.

Если цикл работает вверх через массив (или список, или что-то еще), увеличивающийся счетчик часто будет лучше соответствовать тому, как читатель может думать о том, что делает цикл - закодируйте свой цикл таким образом.

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

Немного подробнее о «возможно» в ответе:

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

С другой стороны, если вы отсчитываете от нуля и используете счетчик в качестве индекса массива, например, вы можете обнаружить, что код работает против архитектуры памяти системы - чтение из памяти часто приводит к « смотреть вперед 'на несколько ячеек памяти после текущего в ожидании последовательного чтения. Если вы работаете в обратном направлении через память, система кэширования может не ожидать чтения из области памяти по более низкому адресу памяти. В этом случае возможно, что зацикливание «в обратном направлении» может снизить производительность. Тем не менее, я бы все равно закодировал цикл таким образом (если производительность не стала проблемой), потому что правильность имеет первостепенное значение, а приведение кода в соответствие с моделью - отличный способ обеспечить правильность. Неправильный код настолько неоптимизирован, насколько это возможно.

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

4
ответ дан 24 November 2019 в 00:19
поделиться

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

for(int i=myCollection.size(); i >= 0; i--)
{
   ...
}

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

1
ответ дан 24 November 2019 в 00:19
поделиться

Дело в том, что при обратном отсчете вам не нужно проверять i> = 0 отдельно для уменьшения i . Обратите внимание:

for (i = 5; i--;) {
  alert(i);  // alert boxes showing 4, 3, 2, 1, 0
}

И сравнение, и уменьшение i могут быть выполнены в одном выражении.

См. Другие ответы, почему это сводится к меньшему количеству инструкций x86.

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

1
ответ дан 24 November 2019 в 00:19
поделиться

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

Согласно этой странице википедии: Секунда координации , «... солнечные сутки становятся длиннее на 1,7 мс с каждым столетием, в основном из-за приливного трения». Но если вы считаете дни до своего дня рождения, неужели вас волнует эта крошечная разница во времени?

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

Я готов поспорить, что большинство программистов прочитают (i = 0; i 0; i--) я должен подумать об этом на мгновение . Лучше всего, если намерение кода попадет прямо в мозг, не требуя никаких размышлений.

2
ответ дан 24 November 2019 в 00:19
поделиться

Как ни странно, похоже, что разница есть. По крайней мере, в 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, а с других произвольное значение. Так что, вероятно, разница не только в сравнении с нулем?

2
ответ дан 24 November 2019 в 00:19
поделиться

На некоторых старых процессорах есть / были такие инструкции, как DJNZ == "уменьшить и перейти, если не ноль". Это позволяло создавать эффективные циклы, в которых вы загружали начальное значение счетчика в регистр, а затем вы могли эффективно управлять циклом уменьшения с помощью одной инструкции. Мы говорим здесь об ISA 1980-х годов - ваш учитель серьезно оторван, если он думает, что это «практическое правило» все еще применимо к современным процессорам.

3
ответ дан 24 November 2019 в 00:19
поделиться

Теперь, я думаю, вам хватило лекций по сборке:) Я хотел бы представить вам еще одну причину для подхода 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);
    }

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

0
ответ дан 24 November 2019 в 00:19
поделиться

Боб,

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

Есть 4 вещи, которые нужно рассмотреть в вашем примере с циклом:

for (i=N; 
 i>=0;             //thing 1
 i--)             //thing 2
{
  putchar('*');   //thing 3
}
  • Сравнение

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

  • Регулировка

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

  • Тело цикла

Вы обращаетесь к системному вызову с помощью putchar. Это очень медленно. Кроме того, вы выводите изображение на экран (косвенно). Это еще медленнее. Подумайте о соотношении 1000:1 или больше. В этой ситуации тело цикла полностью и полностью перевешивает стоимость настройки/сравнения цикла.

  • Кэш

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

3
ответ дан 24 November 2019 в 00:19
поделиться

В 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 каждый раз в цикле.

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

12
ответ дан 24 November 2019 в 00:19
поделиться

Вот что может произойти на некотором оборудовании в зависимости от того, что компилятор может определить о диапазоне используемых вами чисел: с помощью цикла увеличения вы должны test i каждый раз при обходе цикла. Для убывающей версии флаг переноса (установленный как побочный эффект вычитания) может автоматически сообщать вам, если i> = 0 . Это экономит тест на каждый цикл цикла.

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

29
ответ дан 24 November 2019 в 00:19
поделиться

Да!!!

Счет от N до 0 немного быстрее, чем счет от 0 до N в смысле того, как аппаратура будет обрабатывать сравнение...

Обратите внимание на сравнение в каждом цикле

i>=0
i<N

Большинство процессоров имеют инструкцию сравнения с нулем... поэтому первое сравнение будет переведено в машинный код как:

  1. Load i
  2. Compare and jump if Less than or Equal zero

Но во втором случае нужно каждый раз загружать N из памяти

  1. load i
  2. load N
  3. Sub i и N
  4. Compare and jump if Less than or Equal zero

Так что это не из-за отсчета вниз или вверх... А из-за того, как ваш код будет переведен в машинный код...

Поэтому счет от 10 до 100 - это то же самое, что и счет от 100 до 10
. Но счет от i=100 до 0 быстрее, чем от i=0 до 100 - в большинстве случаев
А счет от i=N до 0 быстрее, чем от i=0 до N

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

Связанное: Почему n++ выполняется быстрее, чем n=n+1?

23
ответ дан 24 November 2019 в 00:19
поделиться

независимо от направления всегда используйте префиксную форму (++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)  

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

1
ответ дан 24 November 2019 в 00:19
поделиться

Отсчет идет быстрее в таком случае:

for (i = someObject.getAllObjects.size(); i >= 0; i--) {…}

потому что someObject.getAllObjects.size() выполняется один раз в начале.


Конечно, аналогичного поведения можно добиться, вызывая size() вне цикла, как упоминал Питер:

size = someObject.getAllObjects.size();
for (i = 0; i < size; i++) {…}
6
ответ дан 24 November 2019 в 00:19
поделиться

В наборе инструкций Intel x86 построение цикла для обратного отсчета до нуля обычно можно выполнить с меньшим количеством инструкций, чем цикл, который считает до ненулевого условия выхода.В частности, регистр ECX традиционно используется в качестве счетчика циклов в x86 asm, а в наборе инструкций Intel есть специальная инструкция перехода jcxz, которая проверяет регистр ECX на ноль и выполняет переходы на основе результата теста.

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

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

27
ответ дан 24 November 2019 в 00:19
поделиться
Другие вопросы по тегам:

Похожие вопросы: