繁体   English   中英

在for循环中倒计时

[英]Counting down in for-loops

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

我目前有这个:

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

我的'i'是unsigned resgister int,'domain'也是unsigned int

在for-loop中,i用于遍历数组,例如

array[i] = do stuff

把它转换成倒计时会弄乱我的例行程序的预期/正确输出。

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

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

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

请帮忙。

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

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

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

我知道非常不明显。

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

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

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

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

或者从domain计数到1而不是从domain - 1计数domain - 10

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

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

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

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

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

也就是说,更好地利用优化时间将考虑整体程序结构,使用的数据结构和算法,资源利用率等。

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

在x86上:

dec eax
jnz Foo

代替:

inc eax
cmp eax, 15
jl Foo

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

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

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

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

它与向上向下计数无关。 更快的是朝零 迈克尔的答案显示了为什么 - 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. 在测试和分析显示必要之前,不要以性能的名义做任何事情。

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

#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迭代到0,我们知道数组末尾的元素已被证明对此迭代有效。

有关进一步说明......

如果:

  1. 您可以通过交换数组的一端并更改数组边界来排除坏元素,从而删除坏元素。

然后很明显:

  1. 您将与一个好的元素交换,即在此迭代中已经过测试的元素。

所以这意味着:

  1. 如果我们迭代变量bound,那么变量bound和当前迭代指针之间的元素已被证明是好的。 迭代指针是否获得++或 - 并不重要。 重要的是我们正在迭代变量边界,因此我们知道与它相邻的元素是好的。

最后:

  1. 迭代为0允许我们仅使用一个变量来表示数组边界。 这是否重要是您和编译器之间的个人决定。

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

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--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架构上为我提供了很好的服务。

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

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

当这是一个真正的问题时,我很惊讶这篇文章被评为-1。

重要的是,无论你是在增加还是减少你的计数器,无论你是在上升记忆还是记忆力下降。 大多数缓存都针对内存而非内存进行了优化。 由于内存访问时间是当今大多数程序所面临的瓶颈,这意味着更改程序以便提高内存可以提高性能,即使这需要将计数器与非零值进行比较。 在我的一些程序中,我通过将代码更改为内存而不是内存来看到性能的显着提高。

持怀疑态度? 这是我得到的输出:

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_up上升内存而sum_abs_down下降内存。 我甚至通过引用传递vec ,以便两个函数访问相同的内存位置。 尽管如此, sum_abs_up始终比sum_abs_down快。 自己运行(我用g ++ -O3编译)。

仅供参考我的vec_original进行实验,以便我更容易地改变sum_abs_upsum_abs_down ,使得它们改变vec而不允许这些改变影响未来的时间。

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

关键是,根据经验,如果你有选项,如果循环的体积很小,并且如果让你的循环上升记忆而不是向下记忆之间没有什么区别,那么你应该记忆。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM