Подтвердить что ты не робот

Следует ли оценивать или хранить предел цикла?

В С++ быстрее ли хранить предел цикла в переменной, чем вычислять значение?

Например:

Это более медленный подход к использованию

for(int i=0; i<n*n+2*n; ++i)
{ .... }

чем делать следующее?

for(int i=0, limit=n*n+2*n; i<limit; ++i)
{ .... }

Для ясности предположим, что n - это заданная переменная, которая остается неизменной в течение цикла.

4b9b3361

Ответ 1

Если n - объявленная глобально переменная non volatile, то поведение

for (int i = 0; i < n * n + 2 * n; ++i)

не указывается. Компилятору разрешено оптимизировать n * n + 2 * n для оценки один раз, даже если другой поток изменяет n. Кроме того, если другой поток может изменить n, тогда вы должны предпринять шаги, чтобы избежать возможности одновременного чтения и записи n (поведение которого undefined). Рассмотрим std::atomic<int> как тип для n.

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

for (int i = 0, limit = n * n + 2 * n; i < limit; ++i)

который имеет то преимущество, что область limit не течет в окружающие утверждения.

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

for (int i = n * n + 2 * n - 1; i >= 0; --i)

Будьте очень осторожны, но при использовании типов unsigned, если вы примете эту идею.

Ответ 2

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

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

Сказав это, я предпочитаю этот путь с недавнего времени:

for (int i = 0, upperBound = n*n + 2*n /*or n*(n + 2)*/; i < upperBound; ++i)
{
}

Таким образом, область действия upperBound ограничивается только оператором for и не впадает во внешнюю область, где она не нужна.

Ответ 3

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

for (int i=0; i < n*n+2*n; i++)

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

Ответ 4

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

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

// file foo.cc

extern int non_local_int;               // access can be optimized
extern volatile int volatile_int;       // access must not be optimized
extern int bar(int);                    // may have global side effects
extern void take_addr(int&);            // may store address
namespace {
  int addr_never_taken_int=10;          // never operand of address-of operator
  int addr_taken_int=10;                // used as operand of address-of op.
}

void foo(int n)
{
  for(int i=0; i<n*n+n+n+1; ++i)        // can be optimized 
  { ... }

  int local_int = bar(n);
  for(int i=0; i<n*local_int; ++i)      // can be optimized
  { ... }

  for(int i=0; i<n*non_local_int; ++i)  // can be optimized, but is not threadsafe
  { ... no calls to outside code }

  for(int i=0; i<n*bar(n); ++i)         // cannot be optimized
  { ... }

  for(int i=0; i<addr_never_taken_int; ++i)  // can be optimized
  { ... }

  take_addr(addr_taken_int);
  for(int i=0; i<addr_taken_int; ++i)   // cannot be optimized
  { ... code that calls *any* outside function }

  for(int i=0; i<n*volatile_int; ++i)   // must not be optimized
  { ... }

  for(int i=0; i<n; ++i)                // can be optimized
  { ... code that calls *any* outside function }

  take_addr(n);
  for(int i=0; i<n; ++i)                // cannot be optimized
  { ... code that calls *any* outside function }
}

Отредактировано, чтобы отразить комментарии, сделанные supercat. Обратите внимание, что volatile объекты подходят для связи в одном потоке выполнения (например, с обработчиком сигнала), но не с другим потоком. Threadsafety является ответственностью программиста.

Ответ 5

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

Ответ 6

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

for (int i = n*n + 2*n - 1; i >= 0; --i)
{ ... }

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

Ответ 7

Я пробовал это, используя простой код С# в Visual Studio с помощью отладчика:

for(int i=0, limit=n*n+2*n; i<limit; ++i)

limit присваивается только один раз и в for(int i=0; i<n*n+2*n; ++i), тогда предел вычисляется для каждой итерации. Поэтому, если n>0, то второй случай, когда limit назначен, выполняется довольно быстро в соответствии с моим наблюдением.