有条件地向后或向前迭代的最快方法

时间:2017-02-20 11:12:09

标签: c loops optimization

我至少可以想到三种选择循环方向的方法。

两个循环,开头一个条件(可能是最快的?):

if (!backwards)
  for (int i = 0; i <= end; i++) {
  // code
  }
else
  for (int i = end; i >= 0; i--){
  // code
  }

在许多元素上循环,在里面测试和增量(我用这个):

for (int l = 0; l < max_len; l++) {
  // code
  if (!backward)
    i++;
  else
    i--;
}

使用变量增量和结束值(可能是最差的?)

if (backward)
  inc = -1;
else
  inc = 1;
for (int i = 0; i != end; i += inc) {
  // code
}

哪种方式更快?编译器是否在每种情况下对其进行优化?

2 个答案:

答案 0 :(得分:4)

在没有特定系统的情况下讨论性能并不是很有意义。对于“通用计算机”,这里要考虑的事情是

  • 生成的实际机器代码。更少的CPU滴答可以在任何CPU上提供更快的程序。
  • 分支机构数量。较少的分支意味着更好的分支预测可能性,并且CPU可以利用指令高速缓冲存储器(如果存在)。
  • 循环完成的实际工作。这可能是最重要的部分。假设循环对数组执行某些操作。如果按顺序访问数组,从数据顶部到数据底部,则意味着CPU可以利用数据缓存。

改进机器代码的一种旧方法是尽可能写入向下计数循环,因为这将导致“分支如果为零”指令,这比“如果等于”的分支稍微快一些。然而,这种技术起源于编译器废话时的黑暗时代。使用现代优化编译器,迭代顺序不应成为性能问题。所以这个技巧大多已经过时了。

除此之外,根据系统的不同,不同的循环可能会产生相对较少/效率较低的代码。您可以反汇编不同版本并进行检查,但这是一个非常小的问题。

关于分支,第三个版本显然比其他版本好得多,因为它只包含一个单独的分支 - 对循环迭代器的检查,它给出了循环本身。第一版更糟糕,第二版更糟糕。

根据循环实际执行的操作,第3版可能不是数据缓存的理想选择。不可能说。

总的来说,这两个版本中的一个可能效率最高:

for(size_t i=start; i!=end; i+=inc)

或者

size_t offset = backwards ? n-1 : 0;
for(size_t i=0; i<n; i++)
{
  size_t index = i - offset;
  arr[index] = something;
}

但唯一的方法是实际进行基准测试和反汇编。要做到这一点,您需要指定一个特定的系统。

答案 1 :(得分:1)

根据您选择的选项,我会避免使用方法2,因为它会在关键循环中可能避免的每个元素上添加检查/分支。如果你知道如果你想要的元素元素靠近数组的后面或前面,那么方法1或3可能是最好的。

比较1和3不太直接。我相信在英特尔X86处理器上,性能将与示例1中的for循环相同。即++ i和i + = 1将转换为添加指令,并且NE(不等于)比较将等于LE(小于等于)。但是,一般来说,要确保您需要检查正在使用的处理器/编译器的反汇编。

注意:在这个帖子中还说过,倒计时循环(与0比较)可能会在某些处理器上提供轻微的速度优势。此外,如果使用preincrement ++ i而不是示例中的post增量,某些处理器将获得轻微的性能提升。

顺便说一句:如果要检查数组结构中的所有元素,并且c库是否支持并行性,则可以证明Parallel For是最快的,您可以将元素数除以处理器线程数。

使用带有4个处理器线程的Parallel For的示例:如果您的数组包含1百万个元素并且您有4个线程,则可以让线程1迭代0到249.999,线程2 250,000到499,999,线程3 500,000到749,999 ,并以同时的方式线程4 750,000至999,999。总理论增益将是4倍 - 减去一些开销加上等待最慢线程完成的时间。 (在这种情况下,这个时间量应该是最小的)。