Обратный отсчет в for-loops

Я считаю (из некоторых исследований), что обратный отсчет в for-loops на самом деле более эффективен и быстрее во время выполнения. Мой полный программный код-C++

в настоящее время у меня это:

for (i=0; i<domain; ++i) {

мое " Я " является неподписанным resgister int, также "домен" является unsigned int

В for-loop i используется для прохождения массива, например

array[i] = do stuff

преобразование этого в обратный отсчет испортит ожидаемый / правильный вывод моей подпрограммы.

I могу себе представить, что ответ довольно тривиален,но я не могу понять его.

UPDATE: "do stuff" не зависит от предыдущей или более поздней итерации. Вычисления внутри цикла for независимы для этой итерации i. (Надеюсь, это имеет смысл).

UPDATE: чтобы достичь ускорения выполнения с моим for-loop, я считаю, и если да, удалите неподписанную часть при удалении моего int или какой другой метод?

пожалуйста, помогите.

14 ответов


Я предполагаю, что ваш обратный цикл for выглядит так:

for (i = domain - 1; i >= 0; --i) {

в таком случае, потому что i is без подписи, это всегда быть больше или равен нулю. Когда вы уменьшаете беззнаковую переменную, равную нулю, она обернется очень большим числом. Решение либо сделать i подписано или измените условие в цикле for следующим образом:

for (i = domain - 1; i >= 0 && i < domain; --i) {

или считать с domain to 1 а чем от domain - 1 to 0:

for (i = domain; i >= 1; --i) {
    array[i - 1] = ...; // notice you have to subtract 1 from i inside the loop now
}

существует только один правильный метод цикла назад с использованием беззнакового счетчика:

for( i = n; i-- > 0; )
{
    // Use i as normal here
}

здесь есть трюк, для последней итерации цикла у вас будет i = 1 в верхней части цикла, i-- > 0 проходит, потому что 1 > 0, то i = 0 в теле цикла. На следующей итерации i-- > 0 терпит неудачу, потому что i == 0, поэтому не имеет значения, что постфиксный декремент перекатился через счетчик.

очень не очевидно, я знаю.


Это не ответ на вашу проблему, потому что вы, кажется, не проблема.

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

вы профилировали свою программу, чтобы проверить, что ваш for-loop является узким местом? Если нет, то вам не нужно тратить время на беспокойство об этом. Более того, наличие "я" в качестве "регистра" int, как вы пишете, не имеет реального смысла с точки зрения производительности.

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

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


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

на x86:

dec eax
jnz Foo

вместо:

inc eax
cmp eax, 15
jl Foo

Если у вас есть приличный компилятор будет оптимизировать "считать" так же эффективно, как "отсчет". Просто попробуйте несколько тестов и вы увидите.


Итак, Вы "читаете", что куттинг более эффективен? Мне очень трудно в это поверить, если вы не покажете мне некоторые результаты профилировщика и код. Я могу купить его при некоторых обстоятельствах, но в общем случае нет. Мне кажется, это классический случай преждевременной оптимизации.

ваш комментарий о "register int i" также очень красноречив. В настоящее время компилятор всегда лучше вас знает, как распределять регистры. Не беспокойтесь об использовании ключевого слова register, если вы профилировали ваш код.


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


это не имеет ничего общего с подсчетом до или вниз. Что может быть быстрее подсчета к нулю. Майкла!--15--> показывает почему - x86 обеспечивает сравнение с нулем как неявный побочный эффект многих инструкциях, поэтому после настройки счетчика, вы просто ветку на основе результата, а не делать прямого сравнения. (Возможно, и другие архитектуры делают то же самое; я не знаю.)

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

for i := x to y do
  foo(i);

во внутреннее представление, более похожее на это:

tmp := Succ(y - x);
i := x;
while tmp > 0 do begin
  foo(i);
  Inc(i);
  Dec(tmp);
end;

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

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

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

в любом случае, вы спрашивали о C++, а не о Паскале. Циклы C++ "for "не так легко применить к оптимизации, как циклы Pascal" for", потому что границы циклов Pascal всегда полностью вычисляются до запуска цикла, тогда как циклы C++ иногда зависят от условия остановки и содержимого цикла. Компиляторам C++ необходимо выполнить некоторый объем статического анализа чтобы определить, может ли какой-либо данный цикл соответствовать требованиям для вида преобразования, на которое безусловно претендуют циклы Pascal. Если компилятор C++ выполняет анализ, он может выполнить аналогичное преобразование.

ничто не мешает вам писать свои петли таким образом самостоятельно:

for (unsigned i = 0, tmp = domain; tmp > 0; ++i, --tmp)
  array[i] = do stuff

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

(вы заметили, что мой код выше использовал неподписанные переменные без опасности обертывания в ноль? Использование двух отдельных переменных позволяет это.)

три вещи, чтобы отнять у всего этого:

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

трудно сказать с информацией, но... перевернуть массив и начать обратный отсчет?


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

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

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

// Start out pointing to the last elem in array
pointer_to_array_elem_type p = array + (domain - 1);
for (int i = domain - 1; --i >= 0 ; ) {
     *p-- = (... whatever ...)
}

эта форма использует флаг состояния, который установлен на некоторые процессоры после арифметических операций -- на некоторых архитектурах декремент и тестирование условия ветви могут быть объединены в одну инструкцию. Обратите внимание, что использование predecrement (--i) является ключом здесь -- использование postdecrement (i--) не работал бы что ж.

кроме того,

// Start out pointing *beyond* the last elem in array
pointer_to_array_elem_type p = array + domain;
for (pointer_to_array_type p = array + domain; p - domain > 0 ; ) {
     *(--p) = (... whatever ...)
}

эта вторая форма использует арифметику указателя (адреса). Я редко вижу форму (pointer - int) в эти дни (по уважительной причине), но язык гарантирует, что при вычитании int из Указателя указатель уменьшается на (int * sizeof (*pointer)).

Я еще раз подчеркну, что являются ли эти формы выигрышем для вас, зависит от процессора и компилятора, который вы используете. Они хорошо служили мне на Motorola 6809 и 68000 зодчие.


в некоторых более поздних ядрах arm decrement и compare принимает только одну инструкцию. Это дает уменьшение петель более эффективным, чем увеличение единиц.

Я не знаю, почему нет инструкции increment-compare.

Я удивлен, что этот пост был проголосован -1, когда это настоящая проблема.


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

#define for_range(_type, _param, _A1, _B1) \
    for (_type _param = _A1, _finish = _B1,\
    _step = static_cast<_type>(2*(((int)_finish)>(int)_param)-1),\
    _stop = static_cast<_type>(((int)_finish)+(int)_step); _param != _stop; \
_param = static_cast<_type>(((int)_param)+(int)_step))

теперь вы можете использовать:

for_range (unsigned, i, 10,0)
{
    cout << "backwards i: " << i << endl;
}

for_range (char, c, 'z','a')
{
    cout << c << endl;
}

enum Count { zero, one, two, three }; 

for_range (Count, c, three, zero)
{
    cout << "backwards: " << c << endl;
}

вы можете повторить в любом направлении:

for_range (Count, c, zero, three)
{
    cout << "forward: " << c << endl;
}

цикл

for_range (unsigned,i,b,a)
{
   // body of the loop
}

выдаст следующий код:

 mov esi,b
L1:
;    body of the loop
   dec esi
   cmp esi,a-1
   jne L1 

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

итерация по последнему элементу сначала удобна, когда вы удаляете недопустимые элементы путем замены с концом массива. Для плохих элементов, не прилегающих к концу, мы можем поменять местами конечную позицию, уменьшить конечную границу массива и продолжить итерацию. Если вы должны были повторить к концу, то замена с концом может привести к обмен плохим на плохое. При итерации end to 0 мы знаем, что элемент в конце массива уже доказан для этой итерации.

для дальнейшего объяснения...

Если:

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

тогда очевидно, что:

  1. вы бы поменялись с хорошим элементом, т. е. тем, который уже был протестирован в этой итерации.

таким образом, это означает:

  1. если мы итерируем от переменной bound, то элементы между переменной bound и текущим указателем итерации оказались хорошими. Получает ли указатель итерации ++ или -- не имеет значения. Важно то, что мы итерируем от переменной, поэтому мы знаем, что соседние с ней элементы хороши.

Итак, наконец:

  1. итерации к 0 позволяет использовать только одну переменную для представления границ массива. Имеет ли это значение, это личное решение между вами и вашим компилятором.

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

скептически? Вот результат, который я получил:

sum up   = 705046256
sum down = 705046256
Ave. Up Memory   = 4839 mus
Ave. Down Memory =  5552 mus
sum up   = inf
sum down = inf
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 RAI, class T>
inline void sum_abs_up(RAI first, RAI one_past_last, T &total) {
  T sum = 0;
  auto it = first;
  do {
    sum += *it;
    it++;
  } while (it != one_past_last);
  total += sum;
}

template<class RAI, class T>
inline void sum_abs_down(RAI first, RAI 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;
}

int main() {
  std::size_t num_repititions = 1 << 10;
  {
  typedef int ValueType;
  auto lower = std::numeric_limits<ValueType>::min();
  auto upper = std::numeric_limits<ValueType>::max();
  std::vector<ValueType> vec(1 << 24);

  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 << "sum up   = " << sum_up   << '\n';
  std::cout << "sum down = " << sum_down << '\n';
  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;
  }
  {
  typedef double ValueType;
  auto lower = std::numeric_limits<ValueType>::min();
  auto upper = std::numeric_limits<ValueType>::max();
  std::vector<ValueType> vec(1 << 24);

  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 << "sum up   = " << sum_up   << '\n';
  std::cout << "sum down = " << sum_down << '\n';
  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 0;
}

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

к твоему сведению!--9--> есть ли для экспериментов, чтобы мне было легко изменить sum_abs_up и sum_abs_down таким образом, что заставляет их изменить vec не позволяя этим изменениям влиять на будущие тайминги.

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

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