在for循环中倒计时

时间:2009-04-29 23:35:24

标签: c++ for-loop

我相信(从一些研究读物中),在for循环中倒计时实际上在运行时更有效,更快。我的完整软件代码是C ++

我目前有这个:

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

我的'我'是无符号的resgister int, 'domain'也是unsigned int

for-loop中的

用于遍历数组,例如

array[i] = do stuff

将此转换为倒计时会使我的例程的预期/正确输出混乱。

我可以想象答案是微不足道的,但我无法理解它。

更新:'do stuff'不依赖于之前或之后的迭代。 for循环中的计算与i的迭代无关。 (我希望这是有道理的)。

更新:要使用我的for循环实现运行时加速,我是否倒计时,如果是这样,在删除我的int时删除未签名的部分,或者其他什么方法?

请帮忙。

13 个答案:

答案 0 :(得分:29)

使用无符号计数器只有一种正向循环方法:

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

这里有一个技巧,对于最后一个循环迭代,你将在循环的顶部有i = 1,i--&gt; 0遍,因为1> 0,然后在循环体中i = 0。在下一次迭代中,我 - &gt; 0因为i == 0而失败,所以后缀减量在计数器上滚动并不重要。

我知道非常不明显。

答案 1 :(得分:27)

我猜你的向后循环看起来像这样:

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

在这种情况下,由于i 无符号始终大于或等于零。当您递减一个等于零的无符号变量时,它将回绕到一个非常大的数字。解决方案是使i签名,或者更改for循环中的条件,如下所示:

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

或从domain1而不是从domain - 10

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

答案 2 :(得分:12)

这不是您问题的答案,因为您似乎没有问题。

这种优化完全不相关,应留给编译器(如果完成的话)。

您是否已分析过您的程序以检查您的for循环是否为瓶颈?如果没有,那么你不需要花时间担心这一点。更重要的是,在你写作时,将“i”作为“寄存器”int,从性能的角度来看并没有真正的意义。

即使不知道您的问题域,我也能保证反向循环技术和“register”int计数器对程序的性能有可忽略的影响。请记住,“过早优化是所有邪恶的根源”。

也就是说,优化时间更长的是考虑整体计划结构,使用的数据结构和算法,资源利用率等。

答案 3 :(得分:10)

检查数字是否为零可以比比较更快或更有效。但这是你真正不应该担心的那种微优化 - 几个时钟周期将因任何其他性能问题而大大相形见绌。

在x86上:

dec eax
jnz Foo

而不是:

inc eax
cmp eax, 15
jl Foo

答案 4 :(得分:3)

如果你有一个不错的编译器,它将优化“向上计数”和“倒计时”一样有效。只需尝试一些基准,你就会看到。

答案 5 :(得分:3)

所以你“读”了下来更有效率?除非你向我展示一些分析器结果和代码,否则我觉得很难相信。我可以在某些情况下购买它,但在一般情况下,没有。在我看来,这是一个过早优化的经典案例。

您对“register int i”的评论也很有说服力。如今,编译器总是比你更了解如何分配寄存器。除非您已经对代码进行了分析,否则不要使用register关键字。

答案 6 :(得分:3)

当您循环遍历任何类型的数据结构时,缓存未命中的影响远远大于您的方向。关注内存布局和算法结构的大局而不是微不足道的微优化。

答案 7 :(得分:3)

它与计算 up down 无关。更快的是将计为零Michael's answer显示了为什么 - 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的值相反,对于那些认为自己的循环正在向后运行的程序员而言,不会导致混乱和恐慌。)

这个想法是,即使使用额外的IncDec指令,就运行时间而言,它仍然是一个净胜利,而不是进行明确的比较。 您是否可以注意这种差异可供辩论。

但请注意,转换是编译器自动自动的转换,基于它是否认为转换是有价值的。 编译器通常比优化代码更好,所以不要花太多精力与它竞争。

无论如何,你问的是C ++,而不是Pascal。 C ++“for”循环不太容易将优化应用于Pascal“for”循环,因为Pascal循环的边界总是在循环运行之前完全计算,而C ++循环有时依赖于停止条件和循环内容。 C ++编译器需要进行一些静态分析,以确定任何给定的循环是否符合Pascal循环有条件无条件转换的要求。如果C ++编译器进行分析,那么它可以进行类似的转换。

没有什么能阻止你自己编写循环:

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

执行可能会使代码运行得更快。就像我之前说过的那样,你可能不会注意到。通过手动安排循环来支付的更高成本是您的代码不再遵循既定惯用语。你的循环是一个非常普通的“for”循环,但它不再像某个一样看起来 - 它有两个变量,它们以相反的方向计数,其中一个甚至不用于循环体 - 所以任何人都在阅读你的代码(包括你,一周,一个月或一年后你忘记了你希望实现的“优化”)将需要花费额外的努力证明自己这个循环确实是一个伪装的普通循环。

(您是否注意到我上面的代码使用了无符号变量而没有绕零的危险?使用两个单独的变量允许这样做。)

从这一切中拿走三件事:

  1. 让优化器完成它的工作;总的来说,它比你更好。
  2. 使普通代码看起来很普通,这样特殊代码就不必竞争以获得人们审核,调试或维护它的注意力。
  3. 在测试和分析显示必要之前,不要以性能的名义做任何事情。

答案 8 :(得分:2)

您可以尝试以下方法,哪种编译器可以非常有效地进行优化:

#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 

答案 9 :(得分:1)

很难说有给出的信息,但......反转你的阵列,并倒数?

答案 10 :(得分:1)

Jeremy Ruten正确地指出使用无符号循环计数器是危险的。据我所知,这也是不必要的。

其他人也指出了过早优化的危险。他们是完全正确的。

话虽如此,这是我多年前编程嵌入式系统时使用的一种风格,当时每个字节和每个周期确实都有用。这些表单 对我使用的特定CPU和编译器非常有用,但您的里程可能会有所不同。

// 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 ...)
}

此形式利用了算术运算后在某些处理器上设置的条件标志 - 在某些体系结构上,分支条件的减量和测试可以组合成单个指令。请注意,使用预先减少(--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))

我将再次强调,这些形式是否适合您,取决于您使用的CPU和编译器。他们在摩托罗拉6809和68000架构上为我提供了很好的服务。

答案 11 :(得分:1)

在一些后来的arm内核中,递减和比较只需要一条指令。这使得递减循环比递增循环更有效。

我不知道为什么还没有增量比较指令。

我很惊讶这篇文章在真正的问题上被选为-1。

答案 12 :(得分: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_upsum_abs_down都做同样的事情,并以相同的方式计时,唯一的区别是sum_abs_upsum_abs_down内存下降时会占用内存。我甚至通过引用传递vec,以便两个函数访问相同的内存位置。然而,sum_abs_up始终比sum_abs_down更快。自己动手(我用g ++ -O3编译)。

仅供参考vec_original进行实验,让我可以轻松更改sum_abs_upsum_abs_down,使其更改vec,同时不允许这些更改影响未来的时间。

重要的是要注意我的计时循环有多紧。如果一个循环体很大,那么它的迭代器是否上升或下降都很重要,因为执行循环体的时间可能完全占主导地位。此外,值得一提的是,对于一些罕见的循环,降低内存有时比上升更快。但即使有这样的循环,上升总是比下降总是慢的情况(与内存上升的循环不同,后者通常总是比同等的内存循环更快;少数几次他们甚至快了40 +%)。

关键是,根据经验,如果你有选项,如果循环的体积很小,并且你的循环之间的差别就是内存而不是下降那么你应该记忆。