具有相同索引的循环的性能
Performance of for-loops with same index
在编码时,我遇到了一个问题:
当我不得不使用很多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*/ }
感谢您对的帮助
如果您正在启用优化(如果您不启用,任何关于性能的讨论都是没有意义的),那么就无法推断编译器在这两种情况下会做什么。
答案将取决于:
- 工具链
- 工具链的版本
- 构建工具链时使用了哪些选项
- 循环内部发生了什么
- (相关)循环是否可以展开
- (相关)循环是否真的需要索引(如果你只是索引到数组中,所有提到的
i
通常都会被优化掉) - 。。。等等
以下是如何编写快速代码:
- 编写简洁表达意图的优雅代码
- 检查您的代码是否优雅,是否简洁地表达了您的意图
- 删除错误并返回2
- 启用优化器
- (这一点很重要)等待用户抱怨您的代码太慢
- 如果5没有发生,停止
- 衡量花在哪里的时间最多,并解决这个问题。这不会是你的循环计数器,我可以向你保证
为了记录,你应该这样写:
for(int ind=0; ind < a; ++ind)
因为这更优雅(ind的范围有限),不太可能出错,所以对ind使用预增量(如果ind碰巧成为类类型,性能会更好),并表达意图(ind用于此循环)。
在实践中,重要的是迭代次数和do something
复杂性,而不是索引变量的定义方式。
此外,请考虑优化规则。
- 不优化
- 尚未优化
- 优化前的配置文件
在恐龙在地球上行走的古代,可能会有这样的情况:"当编译器遇到局部变量声明时,在堆栈上为其分配空间"。
这也许就是为什么古代恐龙C只允许在块的顶部声明变量的原因:古代恐龙编译器需要在生成代码之前提前知道所有变量。
然后在80年代左右,优化编译器开始在变量首次使用时为其分配空间。不管该变量实际声明在哪里。这不仅会减少堆栈峰值使用量,还意味着如果函数不使用变量,则根本不需要分配变量。有些编译器甚至会非常有效,将变量分配到CPU寄存器中,而不是放在堆栈上!
从那时起,每个编译器都是这样工作的。所以,除非你从某个博物馆偷了一个编译器,否则这不应该是你需要思考的事情。
在这两个例子中,循环迭代器很可能都分配在CPU寄存器中。我会调用一个为任何一种情况生成较慢代码的编译器。在最坏的情况下,我想一些编译器可能会对不同的变量名感到有点困惑,也许每个循环都使用不同的CPU寄存器——这会使分解后的C代码读起来很困惑,但不会对性能产生任何影响。
正如其他人已经提到的,最佳实践是尽可能减少每个变量的范围,因此应该使用for(int ind=0; ...
。这与效率无关,而是可读性、可维护性、避免不必要的命名空间污染等。唯一需要在循环之前声明循环迭代器的情况是,在循环结束后需要保留值。
判断某件事是否有影响的唯一方法是测量(并注意答案可能因编译器和平台而异)。
不过,我的直觉是编译器会为这两个样本生成相同的代码。
首先,ind
与int
非常接近,以至于您在问题中打错了它,所以变量名的选择不好。使用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风格对处理代码的人有好处。您可以立即看到循环计数器没有在循环之外使用。(编译器也是如此)。
这是防止初始化错误变量等错误的好方法。
- 与多个 for 循环与单个 for 循环 wrt 相关的性能从多映射获取数据
- 基于范围的 for 循环range_declaration中各种说明符之间的性能差异
- C++中循环的性能差异
- C++循环性能的倍数
- 在原始循环上使用boost::irange的性能损失
- OpenMP 嵌套循环处理性能
- 虚拟函数调用的性能作为 for 循环中的上限
- 在 C++ 中使用 OpenMP 并行化两个 for 循环不会提供更好的性能
- C++和Java的字符串循环性能比较
- C++:if 内部循环的性能影响
- 为什么在循环外举起弦会导致性能较慢
- openMp的多个独立for循环的性能问题
- C 的性能11现代风格的循环与老式循环
- 用于C++和性能关键型应用程序中的循环
- 奇怪的OpenCL调用C++上的副作用来提高循环性能
- 为什么asm中的这种差异对性能很重要(在未优化的ptr++与++ptr循环中)
- C# 与循环性能测量的 C++
- 内部评估的布尔条件以获得循环性能
- 局部变量的循环性能
- 用于循环性能差异和编译器优化