具有相同索引的循环的性能

Performance of for-loops with same index

本文关键字:循环 性能 索引      更新时间:2023-10-16

在编码时,我遇到了一个问题:

当我不得不使用很多for循环时,所有循环都在不同的跨度上迭代。如果我只声明一个变量作为索引(示例i),性能(即运行时)会更好吗?还是根本不重要(示例II)?

示例一:

int ind;
for(ind=0; ind < a; ind++) { /*do something*/ }
for(ind=0; ind < b; ind++) { /*to something*/ }
...
for(ind=0; ind < z; ind++) { /*to something*/ }

示例二:

for(int ind=0; ind < a; ind++) { /*do something*/ }
...
for(int ind=0; ind < z; ind++) { /*do something*/ }

感谢您对的帮助

如果您正在启用优化(如果您不启用,任何关于性能的讨论都是没有意义的),那么就无法推断编译器在这两种情况下会做什么。

答案将取决于:

  1. 工具链
  2. 工具链的版本
  3. 构建工具链时使用了哪些选项
  4. 循环内部发生了什么
  5. (相关)循环是否可以展开
  6. (相关)循环是否真的需要索引(如果你只是索引到数组中,所有提到的i通常都会被优化掉)
  7. 。。。等等

以下是如何编写快速代码:

  1. 编写简洁表达意图的优雅代码
  2. 检查您的代码是否优雅,是否简洁地表达了您的意图
  3. 删除错误并返回2
  4. 启用优化器
  5. (这一点很重要)等待用户抱怨您的代码太慢
  6. 如果5没有发生,停止
  7. 衡量花在哪里的时间最多,并解决这个问题。这不会是你的循环计数器,我可以向你保证

为了记录,你应该这样写:

for(int ind=0; ind < a; ++ind)

因为这更优雅(ind的范围有限),不太可能出错,所以对ind使用预增量(如果ind碰巧成为类类型,性能会更好),并表达意图(ind用于此循环)。

在实践中,重要的是迭代次数和do something复杂性,而不是索引变量的定义方式。

此外,请考虑优化规则。

  1. 不优化
  2. 尚未优化
  3. 优化前的配置文件

在恐龙在地球上行走的古代,可能会有这样的情况:"当编译器遇到局部变量声明时,在堆栈上为其分配空间"。

这也许就是为什么古代恐龙C只允许在块的顶部声明变量的原因:古代恐龙编译器需要在生成代码之前提前知道所有变量。

然后在80年代左右,优化编译器开始在变量首次使用时为其分配空间。不管该变量实际声明在哪里。这不仅会减少堆栈峰值使用量,还意味着如果函数不使用变量,则根本不需要分配变量。有些编译器甚至会非常有效,将变量分配到CPU寄存器中,而不是放在堆栈上!

从那时起,每个编译器都是这样工作的。所以,除非你从某个博物馆偷了一个编译器,否则这不应该是你需要思考的事情。

在这两个例子中,循环迭代器很可能都分配在CPU寄存器中。我会调用一个为任何一种情况生成较慢代码的编译器。在最坏的情况下,我想一些编译器可能会对不同的变量名感到有点困惑,也许每个循环都使用不同的CPU寄存器——这会使分解后的C代码读起来很困惑,但不会对性能产生任何影响。

正如其他人已经提到的,最佳实践是尽可能减少每个变量的范围,因此应该使用for(int ind=0; ...。这与效率无关,而是可读性、可维护性、避免不必要的命名空间污染等。唯一需要在循环之前声明循环迭代器的情况是,在循环结束后需要保留值。

判断某件事是否有影响的唯一方法是测量(并注意答案可能因编译器和平台而异)。

不过,我的直觉是编译器会为这两个样本生成相同的代码。

首先,indint非常接近,以至于您在问题中打错了它,所以变量名的选择不好。使用i作为循环索引是一种几乎通用的约定。


任何合适的编译器都会对整个函数范围内的int i进行生存期分析,并看到i=0在开始时将其与以前的值断开。之后i的使用与之前的使用无关,因为无条件赋值不依赖于根据之前的值计算的任何内容。

因此,从优化编译器的角度来看,不应该有区别实际asm输出中的任何差异都应被视为遗漏的优化错误,无论哪一个更糟


在实践中,在我做的一个简单测试中,针对x86-64的gcc 5.3 -O3 -march=haswell为窄范围和函数范围制作了相同的循环。我不得不在循环中使用三个数组来让gcc使用索引寻址模式,而不是递增指针,这很好,因为单寄存器寻址模式在Intel SnB系列CPU上更有效。

它在两个循环中为i重复使用相同的寄存器,而不是保存/恢复另一个保留调用的寄存器(例如r15)。因此,我们可以看到,对函数中更多变量导致寄存器分配更差的潜在担忧实际上不是问题。gcc大部分时间都做得很好。

这是我在godbolt上测试的两个功能(见上面的链接)它们都使用gcc 5.3 -O3编译为相同的asm

#include <unistd.h>
// int dup(int) is a function that the compiler won't have a built-in for
// it's convenient for looking at code with function calls.
void single_var_call(int *restrict dst, const int *restrict srcA,
                     const int *restrict srcB, int a) {
    int i;
    for(i=0; i < a; i++) { dst[i] = dup(srcA[i] + srcB[i]); }
    for(i=0; i < a; i++) { dst[i] = dup(srcA[i]) + srcB[i]+2; }
}
// Even with restrict, gcc doesn't fuse these loops together and skip the first store
// I guess it can't because the called function could have a reference to dst and look at it
void smaller_scopes_call(int *restrict dst, const int *restrict srcA,
                         const int *restrict srcB, int a) {
    for(int i=0; i < a; i++) { dst[i] = dup(srcA[i] + srcB[i]); }
    for(int i=0; i < a; i++) { dst[i] = dup(srcA[i]) + srcB[i]+2; }
}

出于正确性/可读性原因:首选for (int i=...)

限制循环变量范围的C++/C99风格对处理代码的人有好处。您可以立即看到循环计数器没有在循环之外使用。(编译器也是如此)。

这是防止初始化错误变量等错误的好方法。