Это быстрее считать, чем считать?

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

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

лучше, чем:

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

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

19 ответов

Решение

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

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

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

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

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

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

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

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

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

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

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

Да..!!

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

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

i>=0
i<N

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

  1. Загрузить я
  2. Сравните и перепрыгните, если меньше или равно нулю

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

  1. загрузить я
  2. нагрузка N
  3. Sub я и N
  4. Сравните и перепрыгните, если меньше или равно нулю

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

Таким образом, счет от 10 до 100 такой же, как счет от 100 до 10
Но считать от i=100 до 0 быстрее, чем от i=0 до 100 - в большинстве случаев
И считать от i=N до 0 быстрее, чем от i=0 до N

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

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

В C псудо-сборке:

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

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

Обратный отсчет быстрее в случае, как это:

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

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


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

size = someObject.getAllObjects.size();
for (i = 0; i < size; i++) {…}

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

Скептически? Вот вывод, который я получил:

Ave. Up Memory   = 4839 mus
Ave. Down Memory = 5552 mus

Ave. Up Memory   = 18638 mus
Ave. Down Memory = 19053 mus

от запуска этой программы:

#include <chrono>
#include <iostream>
#include <random>
#include <vector>

template<class Iterator, typename T>
void FillWithRandomNumbers(Iterator start, Iterator one_past_end, T a, T b) {
  std::random_device rnd_device;
  std::mt19937 generator(rnd_device());
  std::uniform_int_distribution<T> dist(a, b);
  for (auto it = start; it != one_past_end; it++)
    *it = dist(generator);
  return ;
}

template<class Iterator>
void FillWithRandomNumbers(Iterator start, Iterator one_past_end, double a, double b) {
  std::random_device rnd_device;
  std::mt19937_64 generator(rnd_device());
  std::uniform_real_distribution<double> dist(a, b);
  for (auto it = start; it != one_past_end; it++)
    *it = dist(generator);
  return ;
}

template<class Iterator, class T>
inline void sum_abs_up(Iterator first, Iterator one_past_last, T &total) {
  T sum = 0;
  auto it = first;
  do {
    sum += *it;
    it++;
  } while (it != one_past_last);
  total += sum;
}

template<class Iterator, class T>
inline void sum_abs_down(Iterator first, Iterator one_past_last, T &total) {
  T sum = 0;
  auto it = one_past_last;
  do {
    it--;
    sum += *it;
  } while (it != first);
  total += sum;
}

template<class T>
std::chrono::nanoseconds TimeDown(std::vector<T> &vec, const std::vector<T> &vec_original,
                                  std::size_t num_repititions, T &running_sum) {
  std::chrono::nanoseconds total{0};
  for (std::size_t i = 0; i < num_repititions; i++) {
    auto start_time = std::chrono::high_resolution_clock::now();
    sum_abs_down(vec.begin(), vec.end(), running_sum);
    total += std::chrono::high_resolution_clock::now() - start_time;
    vec = vec_original;
  }
  return total;
}

template<class T>
std::chrono::nanoseconds TimeUp(std::vector<T> &vec, const std::vector<T> &vec_original,
                                std::size_t num_repititions, T &running_sum) {
  std::chrono::nanoseconds total{0};
  for (std::size_t i = 0; i < num_repititions; i++) {
    auto start_time = std::chrono::high_resolution_clock::now();
    sum_abs_up(vec.begin(), vec.end(), running_sum);
    total += std::chrono::high_resolution_clock::now() - start_time;
    vec = vec_original;
  }
  return total;
}


template<class ValueType>
void TimeFunctions(std::size_t num_repititions, std::size_t vec_size = (1u << 24)) {
  auto lower = std::numeric_limits<ValueType>::min();
  auto upper = std::numeric_limits<ValueType>::max();
  std::vector<ValueType> vec(vec_size);

  FillWithRandomNumbers(vec.begin(), vec.end(), lower, upper);
  const auto vec_original = vec;
  ValueType sum_up = 0, sum_down = 0;

  auto time_up   = TimeUp(vec, vec_original, num_repititions, sum_up).count();
  auto time_down = TimeDown(vec, vec_original, num_repititions, sum_down).count();
  std::cout << "Ave. Up Memory   = " << time_up/(num_repititions * 1000) << " mus\n";
  std::cout << "Ave. Down Memory = " << time_down/(num_repititions * 1000) << " mus"
            << std::endl;
  return ;
}

int main() {
  std::size_t num_repititions = 1 << 10;
  TimeFunctions<int>(num_repititions);
  std::cout << '\n';
  TimeFunctions<double>(num_repititions);
  return 0;
}

И то и другое sum_abs_up а также sum_abs_down делать то же самое и рассчитаны они одинаково с той лишь разницей, что sum_abs_up идет вверх память в то время как sum_abs_down идет вниз памяти. Я даже прохожу vec таким образом, чтобы обе функции обращались к одним и тем же ячейкам памяти. тем не менее, sum_abs_up последовательно быстрее, чем sum_abs_down, Запустите его самостоятельно (я скомпилировал его с помощью g ++ -O3).

FYI vec_original есть для экспериментов, чтобы мне было легко изменить sum_abs_up а также sum_abs_down таким образом, что заставляет их изменить vec не позволяя этим изменениям повлиять на будущие сроки.

Важно отметить, насколько напряженным является цикл, который у меня есть. Если тело цикла большое, то, вероятно, не будет иметь значения, будет ли его итератор увеличиваться или уменьшаться в памяти, поскольку время, необходимое для выполнения тела цикла, вероятно, будет полностью доминировать. Также важно отметить, что при некоторых редких циклах уменьшение памяти иногда происходит быстрее, чем увеличение. Но даже с такими циклами редко случается, что повышение всегда было медленнее, чем снижение (в отличие от циклов с маленьким телом, которые увеличивают объем памяти, для которых часто верно обратное; фактически, для небольшой горстки циклов я по времени увеличение производительности при увеличении памяти составило 40+%).

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

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

Это быстрее считать вниз, чем вверх?

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

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

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

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

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

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

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

Боб,

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

В примере с циклом нужно учесть 4 вещи:

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

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

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

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

  • Loop Body

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

  • Тайники

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

Это может быть быстрее.

На процессоре NIOS II, с которым я сейчас работаю, традиционный цикл for

for(i=0;i<100;i++)

производит сборку:

ldw r2,-3340(fp) %load i to r2
addi r2,r2,1     %increase i by 1
stw r2,-3340(fp) %save value of i
ldw r2,-3340(fp) %load value again (???)
cmplti r2,r2,100 %compare if less than equal 100
bne r2,zero,0xa018 %jump

Если мы будем считать вниз

for(i=100;i--;)

мы получаем сборку, которая требует 2 инструкции меньше.

ldw r2,-3340(fp)
addi r3,r2,-1
stw r3,-3340(fp)
bne r2,zero,0xa01c

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

int i,j,a=0;
for(i=100;i--;){
    for(j=10000;j--;){
        a = j+1;
    }
}

Если внутренний цикл записан, как указано выше, время выполнения составляет: 0,12199999999999999734 секунд. Если внутренний цикл записан традиционным способом, время выполнения составляет: 0,17199999999999998623 секунд. Таким образом, обратный цикл на 30% быстрее.

Но: этот тест был сделан со всеми отключенными оптимизациями GCC. Если мы их включим, то компилятор на самом деле умнее, чем ручная оптимизация, и даже сохраняет значение в регистре в течение всего цикла, и мы получили бы такую ​​сборку, как

addi r2,r2,-1
bne r2,zero,0xa01c

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

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

register int i;
for(i=10000;i--;)
{ ... }

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

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

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

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

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

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

Не вдаваясь в подробности, без необходимости использования счетчика циклов и т. Д. Ниже важны только скорость и счетчик циклов (не ноль).

Вот как большинство людей реализуют цикл с 10 итерациями:

int i;
for (i = 0; i < 10; i++)
{
    //something here
}

В 99% случаев это все, что может понадобиться, но наряду с PHP, PYTHON, JavaScript существует целый мир критически важного по времени программного обеспечения (обычно встроенного, ОС, игр и т. Д.), Где такты процессора действительно важны, поэтому кратко рассмотрите код сборки:

int i;
for (i = 0; i < 10; i++)
{
    //something here
}

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

-------- C7 45 B0 00 00 00 00  mov         dword ptr [i],0  
-------- EB 09                 jmp         labelB 
labelA   8B 45 B0              mov         eax,dword ptr [i]  
-------- 83 C0 01              add         eax,1  
-------- 89 45 B0              mov         dword ptr [i],eax  
labelB   83 7D B0 0A           cmp         dword ptr [i],0Ah  
-------- 7D 02                 jge         out1 
-------- EB EF                 jmp         labelA  
out1:

Весь цикл состоит из 8 инструкций (26 байт). В нем - фактически 6 инструкций (17 байт) с 2 ветвями. Да, да, я знаю, что это можно сделать лучше (это всего лишь пример).

Теперь рассмотрим эту частую конструкцию, которую вы часто найдете в написании встроенного разработчика:

i = 10;
do
{
    //something here
} while (--i);

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

00074EBC C7 45 B0 01 00 00 00 mov         dword ptr [i],1  
00074EC3 8B 45 B0             mov         eax,dword ptr [i]  
00074EC6 83 E8 01             sub         eax,1  
00074EC9 89 45 B0             mov         dword ptr [i],eax  
00074ECC 75 F5                jne         main+0C3h (074EC3h)  

5 инструкций (18 байт) и всего одна ветка. На самом деле в цикле 4 инструкции (11 байт).

Лучше всего то, что некоторые процессоры (включая x86/x64-совместимые) имеют инструкцию, которая может уменьшить регистр, затем сравнить результат с нулем и выполнить ветвление, если результат отличается от нуля. Практически ВСЕ процессоры ПК реализуют эту инструкцию. С его помощью цикл фактически представляет собой одну (да, одну) 2-байтовую инструкцию:

00144ECE B9 0A 00 00 00       mov         ecx,0Ah  
label:
                          // something here
00144ED3 E2 FE                loop        label (0144ED3h)  // decrement ecx and jump to label if not zero

Должен ли я объяснить, что быстрее?

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

Поэтому, несмотря на некоторые случаи, которые вы можете указать в качестве комментария, почему я ошибаюсь и т. Д., Я подчеркиваю - ДА, НУЖНО БЫТЬ ЗАКРЫТО, если вы знаете, как, почему и когда.

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

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

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

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

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

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

Посмотрите на эту небольшую часть кода 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);
    }

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

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

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

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

Другие вопросы по тегам