Наш учитель информатики как-то сказал, что почему-то эффективнее считать, чем считать. Например, если вам нужно использовать цикл FOR, а индекс цикла где-то не используется (например, выводит строку N * на экран), я имею в виду такой код:
for (i = N; i >= 0; i--)
putchar('*');
лучше, чем:
for (i = 0; i < N; i++)
putchar('*');
Это правда? И если да, то кто-нибудь знает почему?
c
performance
loops
боб
источник
источник
putchar
использует 99,9999% времени (плюс-минус).i
is unsigned, первый цикл бесконечен?Ответы:
В древние времена, когда компьютеры все еще вручную изготавливали из плавленого кварца, когда 8-битные микроконтроллеры бродили по Земле, и когда ваш учитель был молод (или учитель вашего учителя был молод), существовала обычная машинная инструкция, называемая декрементом и пропуском. если ноль (DSZ). Программисты горячей сборки использовали эту инструкцию для реализации циклов. Более поздние машины получили более изящные инструкции, но все еще оставалось довольно много процессоров, на которых было дешевле сравнивать что-то с нулем, чем сравнивать с чем-либо еще. (Это верно даже для некоторых современных RISC-машин, таких как PPC или SPARC, в которых весь регистр всегда равен нулю.)
Итак, если вы настроите свои петли для сравнения с нулем, а не
N
, что может случиться?Являются ли эти различия , вероятно, приведет к какой - либо измеримое улучшение на реальных программ на современном испорченный процессор? Очень маловероятно. На самом деле, я был бы впечатлен, если бы вы смогли показать ощутимое улучшение даже на микробенчмарке.
Резюме: Я ударил вашего учителя по голове! Вы не должны изучать устаревшие псевдо-факты о том, как организовывать циклы. Вы должны понимать, что самое важное в циклах - это быть уверенными в том, что они завершаются , дают правильные ответы и легко читаются . Я бы хотел, чтобы ваш учитель сосредоточился на самом важном, а не на мифологии.
источник
putchar
занимает на много порядков больше, чем накладные расходы цикла.j=N-i
показывает, что эти два цикла эквивалентны.Вот что может произойти на некотором оборудовании в зависимости от того, что компилятор может сделать вывод о диапазоне используемых вами чисел: при увеличивающемся цикле вы должны тестировать
i<N
каждый раз, когда проходите цикл. Для уменьшающейся версии флаг переноса (установленный как побочный эффект вычитания) может автоматически сообщить вам, еслиi>=0
. Это экономит тест на каждый цикл цикла.В действительности, на современном конвейерном аппаратном обеспечении процессора это почти наверняка не имеет значения, поскольку нет простого отображения 1-1 от инструкций к тактовым циклам. (Хотя я мог представить, что это произойдет, если вы будете делать такие вещи, как генерация точно синхронизированных видеосигналов от микроконтроллера. Но тогда вы все равно будете писать на языке ассемблера.)
источник
В наборе команд Intel x86 построение цикла для обратного отсчета до нуля обычно можно выполнить с меньшим количеством инструкций, чем для цикла, который считает до ненулевого условия выхода. В частности, регистр ECX традиционно используется в качестве счетчика циклов в x86 asm, а в наборе инструкций Intel есть специальная инструкция перехода jcxz, которая проверяет регистр ECX на ноль и выполняет переходы на основе результата теста.
Однако разница в производительности будет незначительной, если ваш цикл уже не очень чувствителен к счетчикам тактовых циклов. Обратный отсчет до нуля может сократить 4 или 5 тактов на каждой итерации цикла по сравнению с обратным отсчетом, так что это скорее новинка, чем полезный метод.
Кроме того, в наши дни хороший оптимизирующий компилятор должен уметь преобразовывать исходный код цикла подсчета в машинный код обратного отсчета (в зависимости от того, как вы используете переменную индекса цикла), поэтому на самом деле нет никаких причин для написания ваших циклов в странные способы просто выжать цикл или два здесь и там.
источник
Да..!!
Подсчет от N до 0 немного быстрее, чем Подсчет от 0 до N в смысле того, как оборудование будет обрабатывать сравнение.
Обратите внимание на сравнение в каждом цикле
Большинство процессоров имеют сравнение с нулевой инструкцией ... поэтому первая из них будет преобразована в машинный код как:
Но второй должен каждый раз загружать N из памяти
Так что это не из-за обратного отсчета или увеличения .. А из-за того, как ваш код будет переведен в машинный код ..
Таким образом, подсчет от 10 до 100 аналогичен подсчету от 100 до 10,
но подсчет от i = 100 до 0 быстрее, чем от i = 0 до 100 - в большинстве случаев
и подсчет от i = N до 0 быстрее, чем от i = От 0 до N
источник
От C до псудо-сборки:
превращается в
пока:
превращается в
Обратите внимание на отсутствие сравнения во второй псудо-сборке. На многих архитектурах есть флаги, которые устанавливаются арифматическими операциями (сложение, вычитание, умножение, деление, увеличение, уменьшение), которые вы можете использовать для переходов. Они часто дают вам то, что по сути является сравнением результата операции с 0 бесплатно. Фактически на многих архитектурах
семантически то же самое, что и
Кроме того, сравнение с 10 в моем примере может привести к худшему коду. 10, возможно, придется жить в регистре, поэтому, если их не хватает, это стоит и может привести к дополнительному коду для перемещения или перезагрузки 10 каждый раз в цикле.
Компиляторы могут иногда переупорядочивать код, чтобы воспользоваться этим, но это часто бывает сложно, потому что они часто не могут быть уверены, что изменение направления в цикле семантически эквивалентно.
источник
i
не используется в цикле, очевидно, вы можете перевернуть его, не так ли?Обратный отсчет происходит быстрее в таком случае:
потому что
someObject.getAllObjects.size()
выполняется один раз в начале.Конечно, аналогичное поведение может быть достигнуто путем
size()
выхода из цикла, как сказал Питер:источник
exec
.Может быть. Но в более чем 99% случаев это не имеет значения, поэтому вы должны использовать наиболее `` разумный '' тест для завершения цикла, и под разумным я подразумеваю, что читателю требуется наименьшее количество размышлений, чтобы выяснить что делает цикл (включая то, что заставляет его останавливаться). Сделайте так, чтобы ваш код соответствовал ментальной (или документированной) модели того, что он делает.
Если цикл работает вверх через массив (или список, или что-то еще), увеличивающийся счетчик часто будет лучше соответствовать тому, как читатель может думать о том, что делает цикл - закодируйте свой цикл таким образом.
Но если вы работаете с контейнером,
N
предметы, и удаляете их по ходу дела, возможно, будет разумнее снизить счетчик.Немного подробнее о «может быть» в ответе:
Это правда, что на большинстве архитектур для проверки вычисления, приводящего к нулю (или переходу от нуля к отрицательному), не требуется явных инструкций по тестированию - результат можно проверить напрямую. Если вы хотите проверить, дает ли результат вычисления какое-то другое число, поток инструкций обычно должен иметь явную инструкцию для проверки этого значения. Однако, особенно с современными ЦП, этот тест обычно добавляет меньше времени, чем уровень шума, к циклической конструкции. В частности, если этот цикл выполняет ввод-вывод.
С другой стороны, если вы отсчитываете от нуля и используете счетчик в качестве, например, индекса массива, вы можете обнаружить, что код работает против архитектуры памяти системы - чтение из памяти часто заставляет кеш «смотреть вперед» несколько ячеек памяти после текущего в ожидании последовательного чтения. Если вы работаете в обратном направлении через память, система кэширования может не ожидать чтения из области памяти по более низкому адресу памяти. В этом случае возможно, что «обратный цикл» может снизить производительность. Тем не менее, я бы, вероятно, закодировал цикл таким образом (если производительность не стала проблемой), потому что правильность имеет первостепенное значение, а приведение кода в соответствие с моделью - отличный способ обеспечить правильность. Неправильный код настолько неоптимизирован, насколько это возможно.
Поэтому я бы склонен забыть совет профессора (конечно, не о его тесте - вы все равно должны быть прагматичными в классе) до тех пор, пока производительность кода действительно не будет иметь значения.
источник
На некоторых старых процессорах есть / были такие инструкции, как
DJNZ
== «уменьшить и перейти, если не ноль». Это позволяло создавать эффективные циклы, когда вы загружали начальное значение счетчика в регистр, а затем вы могли эффективно управлять циклом уменьшения с помощью одной инструкции. Мы говорим здесь об ISA 1980-х годов - ваш учитель серьезно потерял связь, если считает, что это «практическое правило» все еще применимо к современным процессорам.источник
Боб,
Нет, пока вы не выполните микрооптимизацию, и тогда у вас будет под рукой руководство для вашего процессора. Более того, если бы вы занимались подобными вещами, вам, вероятно, все равно не пришлось бы задавать этот вопрос. :-) Но ваш учитель, видимо, не разделяет эту идею ....
В примере с циклом следует учитывать 4 вещи:
Сравнение (как указывали другие) относится к конкретным архитектурам процессоров . Есть больше типов процессоров, чем те, которые работают под Windows. В частности, может быть инструкция, которая упрощает и ускоряет сравнение с 0.
В некоторых случаях быстрее настроить вверх или вниз. Обычно хороший компилятор выясняет это и, если может, повторяет цикл. Однако не все компиляторы хороши.
Вы получаете доступ к системному вызову с помощью putchar. Это очень медленно. Кроме того, вы выполняете рендеринг на экране (косвенно). Это еще медленнее. Подумайте о соотношении 1000: 1 или больше. В этой ситуации тело цикла полностью перевешивает затраты на настройку / сравнение цикла.
Расположение кэша и памяти может иметь большое влияние на производительность. В этой ситуации это не имеет значения. Однако, если вы обращались к массиву и нуждались в оптимальной производительности, вам следовало бы изучить, как ваш компилятор и ваш процессор распределяют доступ к памяти, и настроить свое программное обеспечение, чтобы максимально использовать это. Стандартный пример приведен в отношении умножения матриц.
источник
Гораздо важнее, чем увеличиваете вы или уменьшаете счетчик, так это то, увеличиваете вы или уменьшаете память. Большинство кешей оптимизированы для увеличения объема памяти, а не ее уменьшения. Поскольку время доступа к памяти является узким местом, с которым сегодня сталкивается большинство программ, это означает, что изменение вашей программы таким образом, чтобы вы увеличивали объем памяти, может привести к повышению производительности, даже если для этого потребуется сравнение вашего счетчика с ненулевым значением. В некоторых из моих программ я заметил значительное улучшение производительности, изменив код так, чтобы он увеличивал объем памяти, а не сокращал ее.
Скептически? Просто напишите программу для циклов увеличения / уменьшения памяти. Вот результат, который я получил:
(где mus означает микросекунды) от запуска этой программы:
Оба
sum_abs_up
иsum_abs_down
делают одно и то же (суммируют вектор чисел) и синхронизируются одинаково, с той лишь разницей, чтоsum_abs_up
память увеличивается, а памятьsum_abs_down
уменьшается. Я даже передаюvec
по ссылке, чтобы обе функции обращались к одним и тем же ячейкам памяти. Тем не менее,sum_abs_up
постоянно быстрее, чемsum_abs_down
. Попробуйте сами (я скомпилировал его с помощью g ++ -O3).Важно отметить, насколько тугая петля, которую я рассчитываю. Если тело цикла велико, то, вероятно, не будет иметь значения, будет ли его итератор увеличивать или уменьшать память, поскольку время, необходимое для выполнения тела цикла, скорее всего, будет полностью доминировать. Кроме того, важно отметить, что в некоторых редких циклах уменьшение памяти иногда происходит быстрее, чем ее увеличение. Но даже с такими циклами никогда не было случая, чтобы увеличение памяти всегда медленнее, чем уменьшение (в отличие от небольших циклов, которые увеличивают объем памяти, для чего часто бывает наоборот; фактически, для небольшой горстки циклов I '' При этом прирост производительности за счет увеличения объема памяти составил 40+%).
Дело в том, что, как показывает опыт, если у вас есть возможность, если тело цикла маленькое, и если есть небольшая разница между тем, чтобы ваш цикл поднимался по памяти, а не опускался, тогда вам следует увеличить память.
FYI
vec_original
предназначен для экспериментов, чтобы упростить изменениеsum_abs_up
иsum_abs_down
сделать так, чтобы они изменилисьvec
, не позволяя этим изменениям влиять на будущие сроки. Я настоятельно рекомендую поэкспериментироватьsum_abs_up
иsum_abs_down
рассчитать результаты.источник
независимо от направления всегда используйте префиксную форму (++ i вместо i ++)!
или
Объяснение: http://www.eskimo.com/~scs/cclass/notes/sx7b.html
Кроме того, вы можете написать
Но я ожидаю, что современные компиляторы смогут делать именно эти оптимизации.
источник
Это интересный вопрос, но с практической точки зрения я не думаю, что он важен и не делает один цикл лучше другого.
Согласно этой странице википедии: « Секунда координации» , «... солнечный день становится на 1,7 мс длиннее каждый век, в основном из-за приливного трения». Но если вы считаете дни до своего дня рождения, разве вас волнует эта крошечная разница во времени?
Более важно, чтобы исходный код был легким для чтения и понимания. Эти два цикла являются хорошим примером того, почему важна удобочитаемость - они не повторяются одинаковое количество раз.
Я готов поспорить, что большинство программистов прочитают (i = 0; i <N; i ++) и сразу поймут, что это повторяется N раз. Цикл (i = 1; i <= N; i ++), в любом случае, для меня немного менее понятен, и с (i = N; i> 0; i--) я должен подумать об этом на мгновение , Лучше всего, если намерение кода попадет прямо в мозг, не требуя никаких размышлений.
источник
Как ни странно, похоже, что разница есть. По крайней мере, в PHP. Рассмотрим следующий тест:
Интересны результаты:
Если кто знает почему, было бы неплохо узнать :)
РЕДАКТИРОВАТЬ : результаты такие же, даже если вы начинаете считать не с 0, а с другого произвольного значения. Значит, разница не только в сравнении с нулем?
источник
Это может быть быстрее.
На процессоре NIOS II, с которым я сейчас работаю, традиционный цикл for
производит сборку:
Если мы обратим отсчет
получаем сборку, которой нужно на 2 инструкции меньше.
Если у нас есть вложенные циклы, в которых внутренний цикл выполняется много, мы можем получить ощутимую разницу:
Если внутренний цикл написан так, как указано выше, время выполнения составляет: 0,12199999999999999734 секунды. Если внутренний цикл записан традиционным способом, время выполнения будет: 0,17199999999999998623 секунды. Таким образом, обратный отсчет цикла выполняется примерно на 30% быстрее.
Но: этот тест был сделан с отключенными всеми оптимизациями GCC. Если мы их включим, компилятор на самом деле умнее, чем эта ручная оптимизация, и даже сохранит значение в регистре в течение всего цикла, и мы получим сборку вроде
В этом конкретном примере компилятор даже замечает, что переменная a всегда будет равна 1 после выполнения цикла, и полностью пропускает циклы.
Однако я испытал, что иногда, если тело цикла достаточно сложно, компилятор не может выполнить эту оптимизацию, поэтому самый безопасный способ всегда получить быстрое выполнение цикла - это написать:
Конечно, это работает только в том случае, если не имеет значения, что цикл выполняется в обратном порядке и, как сказал Betamoo, только если вы ведете обратный отсчет до нуля.
источник
То, что сказал ваш учитель, было косвенным утверждением без особых пояснений. Это НЕ то, что уменьшение происходит быстрее, чем увеличение, но вы можете создать гораздо более быстрый цикл с уменьшением, чем с приращением.
Не вдаваясь в подробности, без использования счетчика циклов и т. Д. - ниже важны только скорость и количество циклов (ненулевое).
Вот как большинство людей реализуют цикл с 10 итерациями:
В 99% случаев это все, что может понадобиться, но наряду с PHP, PYTHON, JavaScript существует целый мир критичного ко времени программного обеспечения (обычно встроенного, ОС, игр и т. Д.), Где тики процессора действительно имеют значение, поэтому кратко ознакомьтесь с кодом сборки:
после компиляции (без оптимизации) скомпилированная версия может выглядеть так (VS2015):
Весь цикл состоит из 8 инструкций (26 байт). В нем - фактически 6 инструкций (17 байт) с 2 ветвями. Да, да, я знаю, что это можно сделать лучше (это просто пример).
Теперь рассмотрим эту частую конструкцию, которую вы часто найдете написанной встроенным разработчиком:
Он также повторяется 10 раз (да, я знаю, что значение i отличается от показанного в цикле for, но здесь мы заботимся о количестве итераций). Это может быть скомпилировано в это:
5 инструкций (18 байт) и всего одна ветка. Фактически в цикле 4 инструкции (11 байтов).
Лучше всего то, что некоторые процессоры (включая x86 / x64-совместимые) имеют инструкцию, которая может уменьшать регистр, позже сравнивать результат с нулем и выполнять переход, если результат отличен от нуля. Практически ВСЕ процессоры ПК реализуют эту инструкцию. Используя его, цикл фактически представляет собой одну (да, одну) 2-байтовую инструкцию:
Мне нужно объяснять, что быстрее?
Теперь, даже если конкретный процессор не реализует указанную выше инструкцию, все, что требуется для эмуляции, это декремент с последующим условным переходом, если результат предыдущей инструкции оказывается нулевым.
Итак, независимо от некоторых случаев, которые вы можете указать в качестве комментария, почему я ошибаюсь, и т. Д., Я ПОДЧЕРКНУЮ - ДА, ВЫГОДНО ПЕРЕЙТИ ВНИЗ, если вы знаете, как, почему и когда.
PS. Да, я знаю, что мудрый компилятор (с соответствующим уровнем оптимизации) перепишет цикл for (с возрастающим счетчиком цикла) в эквивалент do .. while для итераций постоянного цикла ... (или развернет его) ...
источник
Нет, это не совсем так. Одна ситуация, когда это могло бы быть быстрее, - это когда вы в противном случае вызывали бы функцию для проверки границ во время каждой итерации цикла.
Но если делать это таким образом менее ясно, это не имеет смысла. В современных языках вы в любом случае должны использовать цикл foreach, когда это возможно. Вы специально упоминаете случай, когда вам следует использовать цикл foreach - когда вам не нужен index.
источник
for(int i=0, siz=myCollection.size(); i<siz; i++)
.Дело в том, что при обратном отсчете не нужно проверять
i >= 0
отдельно для уменьшенияi
. Заметим:И сравнение, и уменьшение
i
могут быть выполнены в одном выражении.Посмотрите другие ответы, почему это сводится к меньшему количеству инструкций x86.
Что касается того, имеет ли это значение для вашего приложения, я полагаю, это зависит от того, сколько у вас циклов и насколько глубоко они вложены. Но для меня это так же легко читать, так что я все равно это делаю.
источник
Теперь, я думаю, у вас было достаточно лекций по сборке :) Я хотел бы представить вам еще одну причину для подхода сверху-> вниз.
Причина пойти сверху очень проста. В теле цикла вы можете случайно изменить границу, что может закончиться некорректным поведением или даже незавершенным циклом.
Взгляните на эту небольшую часть кода Java (по этой причине, я думаю, язык не имеет значения):
Итак, я хочу сказать, что вам следует подумать о том, чтобы предпочесть идти сверху вниз или использовать константу в качестве границы.
источник
for (int i=0; i < 999; i++) {
.for(int xa=0; xa<collection.size(); xa++) { collection.add(SomeObject); ... }
На уровне ассемблера цикл, который ведет отсчет до нуля, обычно немного быстрее, чем цикл, который ведет отсчет до заданного значения. Если результат вычисления равен нулю, большинство процессоров установят нулевой флаг. Если при вычитании единицы вычисление оборачивается вокруг нуля, это обычно изменяет флаг переноса (на некоторых процессорах он устанавливается, на других он сбрасывается), поэтому сравнение с нулем происходит практически бесплатно.
Это еще более верно, когда количество итераций не константа, а переменная.
В тривиальных случаях компилятор может быть в состоянии оптимизировать направление счета цикла автоматически, но в более сложных случаях программист может знать, что направление цикла не имеет отношения к общему поведению, но компилятор не может этого доказать.
источник