Подсчет чисел для циклов
Я полагаю (из некоторых исследований), что подсчет в for-loops на самом деле более эффективен и быстрее во время выполнения. Мой полный программный код - С++
В настоящее время у меня есть это:
for (i=0; i<domain; ++i) {
my 'i' - это unsigned resgister int,
также 'domain' является unsigned int
в цикле for я используется для прохождения массива, например.
array[i] = do stuff
преобразуя это значение в обратный отсчет, помешает ожидаемый/правильный вывод моей подпрограммы.
Я могу представить, что ответ довольно тривиален, но я не могу обойти его.
UPDATE: "do stuff" не зависит от предыдущей или более поздней итерации. Вычисления внутри цикла for являются независимыми для этой итерации i. (Надеюсь, это имеет смысл).
UPDATE: для достижения ускорения во время выполнения с помощью цикла for-loop, я рассчитываю и, если это так, удаляет неподписанную часть при деляровании моего int или какого-либо другого метода?
Пожалуйста, помогите.
Ответы
Ответ 1
Я предполагаю, что ваш обратный цикл выглядит так:
for (i = domain - 1; i >= 0; --i) {
В этом случае, поскольку i
без знака, он будет всегда быть больше или равен нулю. Когда вы уменьшаете переменную без знака, равную нулю, она будет обтекаться до очень большого числа. Решением является либо сделать i
подписанный, либо изменить условие в цикле for следующим образом:
for (i = domain - 1; i >= 0 && i < domain; --i) {
Или рассчитывайте от domain
до 1
, а не от domain - 1
до 0
:
for (i = domain; i >= 1; --i) {
array[i - 1] = ...; // notice you have to subtract 1 from i inside the loop now
}
Ответ 2
Существует только один правильный метод циклического возврата назад с использованием счетчика без знака:
for( i = n; i-- > 0; )
{
// Use i as normal here
}
Здесь есть трюк, для последней итерации цикла у вас будет я = 1 в верхней части цикла, я → 0, потому что 1 > 0, а затем я = 0 в теле цикла. На следующей итерации i- > 0 терпит неудачу, потому что я == 0, поэтому не имеет значения, что посткризисный декремент перевернулся через счетчик.
Очень неясно, что я знаю.
Ответ 3
Это не ответ на вашу проблему, потому что у вас нет проблем.
Этот вид оптимизации совершенно не имеет значения и должен быть оставлен компилятору (если это вообще делается).
Профилировали ли вы свою программу, чтобы проверить, что ваш for-loop является узким местом? Если нет, то вам не нужно тратить время на беспокойство об этом. Тем более, что "i" как "регистр" int, как вы пишете, не имеет никакого реального смысла с точки зрения производительности.
Даже не зная вашего проблемного домена, я могу гарантировать, что как метод обратного цикла, так и счетчик "register" int окажут незначительное влияние на производительность вашей программы. Помните: "Преждевременная оптимизация - это корень всего зла".
Тем не менее, лучшее время для оптимизации было бы связано с общей структурой программы, структурами данных и используемыми алгоритмами, использованием ресурсов и т.д.
Ответ 4
Проверка того, является ли число равным нулю, может быть более быстрым или более эффективным, чем сравнение. Но это своего рода микро-оптимизация, о которой вы действительно не должны беспокоиться - несколько тактовых циклов будут сильно затмеваны практически любой другой проблемой.
В x86:
dec eax
jnz Foo
Вместо:
inc eax
cmp eax, 15
jl Foo
Ответ 5
Если у вас есть достойный компилятор, он будет оптимизировать "подсчет" так же эффективно, как "подсчет". Просто попробуйте несколько тестов, и вы увидите.
Ответ 6
Итак, вы "читаете", что couting down более эффективен? Мне очень трудно поверить, если вы не покажете мне некоторые результаты профилировщика и код. Я могу купить его при некоторых обстоятельствах, но в общем случае нет. Мне кажется, что это классический случай преждевременной оптимизации.
Ваш комментарий о "register int i" также очень полезен. В настоящее время компилятор всегда лучше знает, как распределять регистры. Не используйте использование ключевого слова register, если вы не профилировали свой код.
Ответ 7
Когда вы перебираете структуры данных любого типа, промахи в кеше оказывают гораздо большее влияние, чем направление, в котором вы собираетесь двигаться. Позаботьтесь о большей картине макета памяти и структуре алгоритма вместо тривиальных микрооптимизаций.
Ответ 8
Он не имеет ничего общего с подсчетом вверх или вниз. То, что может быть быстрее, означает к нулю. Майкл ответ показывает, почему - x86 дает вам сравнение с нулем в качестве неявного побочного эффекта многих инструкций, поэтому после того, как вы настроите свой счетчик, вы просто ответите на результат делать явное сравнение. (Возможно, другие архитектуры тоже это делают, я не знаю.)
Компиляторы Borland Pascal известны тем, что выполняют эту оптимизацию. Компилятор преобразует этот код:
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
она по-прежнему является чистой победой с точки зрения времени выполнения, делая явное сравнение. Если вы действительно заметите, что разница для обсуждения.
Но обратите внимание, что преобразование - это то, что компилятор выполнил бы автоматически, исходя из того, считал ли он целесообразным преобразование. Компилятор обычно лучше оптимизирует код, чем вы, поэтому не тратьте на него слишком много усилий.
В любом случае, вы спросили о С++, а не Pascal. С++ "для" циклов не так просто применять, что оптимизация для Pascal для "циклов" связана с тем, что границы циклов Pascal всегда полностью вычисляются до начала цикла, тогда как циклы С++ иногда зависят от условия остановки и цикла содержание. Компиляторы С++ должны сделать некоторый объем статического анализа, чтобы определить, может ли какой-либо данный цикл соответствовать требованиям к типу преобразования. Пешки Pascal квалифицируются безоговорочно. Если компилятор С++ выполняет анализ, он может сделать аналогичное преобразование.
Ничего не мешает вам писать свои петли таким образом:
for (unsigned i = 0, tmp = domain; tmp > 0; ++i, --tmp)
array[i] = do stuff
Это может привести к тому, что ваш код будет работать быстрее. Как я уже говорил, вы, вероятно, не заметите. Большие затраты, которые вы платите, вручную упорядочивая свои петли, это то, что ваш код больше не следует установленным идиомам. Ваша петля является совершенно обычным циклом "для", но она больше не выглядит как одна - она имеет две переменные, они подсчитываются в противоположных направлениях, а одна из них даже не используется в теле цикла, поэтому любой, кто читает ваши кода (включая вас, неделю, месяц или год, когда вы забыли "оптимизацию", которую вы надеялись достичь) придется потратить дополнительные усилия, доказывая себе, что петля действительно обычная петля в маскировке.
(Вы заметили, что мой код выше использовал неподписанные переменные без опасности обертывания вокруг нуля? Использование двух отдельных переменных позволяет это.)
Три вещи, чтобы отнять все это:
- Пусть оптимизатор выполняет свою работу; в целом это лучше, чем вы.
- Сделать обычный код обычным, чтобы специальный код не конкурировал, чтобы привлечь внимание от людей, просматривающих, отлаживающих или поддерживающих его.
- Не делайте ничего фантастического в названии производительности до тех пор, пока тестирование и профилирование не покажут, что это необходимо.
Ответ 9
Сложно сказать с информацией, но... отменить свой массив и отсчитать?
Ответ 10
Джереми Рутен справедливо указал, что использование счетчика без знака является опасным. Насколько мне известно, это также необязательно.
Другие также указали на опасность преждевременной оптимизации. Они абсолютно правы.
С учетом сказанного, вот стиль, который я использовал при программировании встроенных систем много лет назад, когда каждый байт и каждый цикл действительно рассчитывали на что-то. Эти формы были полезны для меня на конкретных процессорах и компиляторах, которые я использовал, но ваш пробег может отличаться.
// 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 ...)
}
В этой форме используется флаг условия, установленный на некоторых процессорах после арифметических операций - на некоторых архитектурах, декремент и тестирование условия ветвления могут быть объединены в одну команду. Обратите внимание, что использование preecrement (--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.
Ответ 11
В некоторых более поздних ядрах сердечника декремент и сравнение принимают только одну инструкцию. Это делает декрементирующие циклы более эффективными, чем инкрементальные.
Я не знаю, почему нет инструкции по увеличению приращения.
Я удивлен, что этот пост был проголосован -1, когда это действительно проблема.
Ответ 12
Вы можете попробовать следующее, какой компилятор будет оптимизировать очень эффективно:
#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
Ответ 13
Все здесь сосредоточены на производительности. Существует фактически логическая причина для итерации к нулю, что может привести к более чистым кодам.
Итерация по первому элементу сначала удобна, когда вы удаляете недопустимые элементы путем замены в конце массива. Для плохих элементов, не прилегающих к концу, мы можем поменяться местами в конечном положении, уменьшить конечную границу массива и продолжать повторять. Если вы должны были продолжить итерацию до конца, то обмен с окончанием может привести к плохому обмену. Итерируя конец до 0, мы знаем, что элемент в конце массива уже доказан для этой итерации.
Для дальнейшего объяснения...
Если:
- Вы удаляете плохие элементы путем обмена с одним концом массива и изменения границ массива для исключения плохих элементов.
Тогда, очевидно,
- Вы бы обменялись хорошим элементом, то есть тем, который уже был протестирован на этой итерации.
Итак, это означает:
- Если мы переходим от границы переменной, то элементы между привязкой переменной и текущим указателем итерации доказаны. Указывает ли указатель итерации ++ или - не имеет значения. Важно то, что мы итерации от переменной связаны, поэтому мы знаем, что смежные с ним элементы хороши.
Итак, наконец:
- Итерация в направлении 0 позволяет нам использовать только одну переменную для представления границ массива. Неважно, это ли личное решение между вами и вашим компилятором.
Ответ 14
Что важнее, чем увеличение или уменьшение вашего счетчика - это то, происходит ли вы вверх или вниз по памяти. Большинство кешей оптимизированы для увеличения объема памяти, а не памяти. Поскольку время доступа к памяти является узким местом, с которым сталкиваются сегодня большинство программ, это означает, что изменение вашей программы, так что вы поднимаете память, может привести к повышению производительности, даже если это требует сравнения вашего счетчика с ненулевым значением. В некоторых из моих программ я увидел значительное улучшение производительности, изменив свой код, чтобы перейти вверх, а не вниз.
Скептически? Вот результат, который я получил:
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).
FYI vec_original
существует для экспериментов, чтобы было легко изменить sum_abs_up
и sum_abs_down
таким образом, чтобы они меняли vec
, не позволяя этим изменениям влиять на будущие тайминги.
Важно отметить, насколько жестким является цикл, в котором я настроен. Если тело цикла велико, то, вероятно, не будет иметь значения, будет ли его итератор подниматься вверх или вниз, так как время, необходимое для выполнения тела цикла, вероятно, будет полностью доминировать. Кроме того, важно отметить, что с некоторыми редкими циклами уменьшение объема памяти иногда происходит быстрее, чем при подъеме. Но даже с такими циклами редко случается, что рост всегда был медленнее, чем снижение (в отличие от циклов, которые растут в памяти, которые часто всегда бывают быстрее, чем эквивалентные петли с обратной записью, а в несколько раз их было даже 40 +% быстрее).
Точка, как правило, если у вас есть опция, если тело цикла невелико, и если есть небольшая разница между тем, что ваш цикл поднимается вверх, а не вниз, тогда вы должны перейти в память.