具有相同索引的 for 循环的性能
Posted
技术标签:
【中文标题】具有相同索引的 for 循环的性能【英文标题】:Performance of for-loops with same index 【发布时间】:2016-03-29 11:25:48 【问题描述】:在编码时我遇到了一个问题:
当我必须使用大量 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】:如果您启用了优化(如果您不启用,任何关于性能的讨论都没有实际意义),那么就无法推断编译器在这两种情况下会做什么。
答案取决于:
-
工具链
工具链的版本
构建工具链时使用了哪些选项
循环内部发生了什么
(相关)循环是否可以展开
(相关)循环是否真的需要索引(如果您只是索引到数组中,所有提到的
i
通常都会被优化掉)。
...等
以下是如何编写快速代码:
-
编写优雅的代码,简洁地表达您的意图。
检查您的代码是否优雅并且简洁地表达了您的意图。
删除错误并返回 2
启用优化器。
(这一点很重要)等待用户抱怨您的代码太慢。
如果 5 没有发生,请停止。
衡量花费最多时间的地方并解决该问题。这不会是你的循环计数器,我可以向你保证。
为了记录,你应该这样写:
for(int ind=0; ind < a; ++ind)
因为它更优雅(ind 的范围有限),不太可能出现错误,对 ind 使用预增量(如果 ind 恰好成为类类型,性能会更好)并表达意图(ind 用于此循环)。
【讨论】:
@MartinBonner 已修复 :-) 我注意到了——我们在以太中穿越了。不过就个人而言,我更喜欢将循环中的条件表示为ind < a
- 编译器往往不喜欢将类型与值进行比较:-)【参考方案2】:
实际上,重要的是迭代次数和do something
复杂度,而不是索引变量的定义方式。
另外,请考虑Rules Of Optimization。
不要优化 尚未优化 优化前的配置文件
【讨论】:
【参考方案3】:古代恐龙在地球上行走的时候,可能会有这样的说法:“当编译器遇到局部变量声明时,在堆栈上为其分配空间”。
这可能是古代恐龙 C 只允许在块顶部声明变量的原因:古代恐龙编译器需要在生成代码之前提前知道所有变量。
然后在 80 年代左右的某个地方,优化编译器开始为变量分配空间在它第一次使用的时候。无论该变量实际上是在哪里声明的。这不仅会减少堆栈峰值的使用,还意味着如果函数不使用变量,则根本不需要分配变量。一些编译器甚至会疯狂有效地将变量分配到 CPU 寄存器中,而不是把它放在堆栈上!
从那时起,每个编译器都是这样工作的。所以除非你从某个博物馆偷了一个编译器,否则这不应该是你需要考虑的事情。
在这两个示例中,您的循环迭代器很可能会被分配到 CPU 寄存器中。我会调用一个编译器,它会为任何一种情况都生成较慢的代码。在最坏的情况下,我想一些编译器可能会对不同的变量名称感到困惑,并且可能会为每个循环使用不同的 CPU 寄存器——这会使反汇编的 C 代码难以阅读,但不会对性能产生任何影响。
正如其他人已经提到的,最佳做法是尽可能缩小每个变量的范围,因此您应该使用for(int ind=0; ...
。这与效率无关,而是可读性、可维护性、避免不必要的命名空间污染等。唯一需要在循环之前声明循环迭代器的情况是在循环结束后需要保留该值。
【讨论】:
据我了解,编译器不会生成一旦超出范围就释放大型本地数组的代码的一个原因是调试格式取决于知道每个函数生成的堆栈帧有多大.我读过一次,Linux(内核)中__attribute__((noinline))
的一个用例是避免在快速路径非错误情况不需要时在堆栈上保留空间(甚至保存/恢复一些寄存器) .所以在某些情况下,编译器可以通过在分支之后才保存一些 reg 来编写更好的代码,但我想很难选择什么时候会好。【参考方案4】:
判断某件事是否有所作为的唯一方法是测量(并注意答案可能因编译器和平台而异)。
我的直觉是编译器会为这两个示例生成相同的代码。
【讨论】:
【参考方案5】:首先,ind
与int
非常接近,以至于您在问题中打错了字,所以这是一个错误的变量名选择。使用i
作为循环索引是一种近乎普遍的约定。
任何体面的编译器都会对整个函数范围内的int i
进行生命周期分析,并看到一开始的i=0
将它与之前的值断开连接。之后的 i
的使用与之前的使用无关,因为无条件赋值不依赖于从先前值计算的任何内容。
所以从优化编译器的角度来看,不应该有区别。 实际 asm 输出的任何差异都应被视为错过优化错误,无论哪个更糟。
在实践中,gcc 5.3 -O3 -march=haswell
targeting x86-64 makes identical loops for narrow scope vs. function scope in a simple test I did。我必须在循环中使用三个数组来让 gcc 使用索引寻址模式而不是递增指针,which is good because one-register addressing modes are more efficient on Intel SnB-family CPUs。
它在两个循环中为i
重复使用相同的寄存器,而不是保存/恢复另一个保留调用的寄存器(例如r15
)。因此,我们可以看到这种潜在的担心函数中的更多变量导致更差的寄存器分配实际上并不是问题。 gcc 大部分时间都做得很好。
这是我在 Godbolt 上测试的两个功能(见上面的链接)。 They both compile to identical asm with gcc 5.3 -O3
.
#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 循环的性能的主要内容,如果未能解决你的问题,请参考以下文章
C# - 在for循环中使用相同的列表大小,索引超出了数组的范围[重复]
循环并合并具有相同索引、相同列的 DataFrame(但是每个 DataFrame 有几列唯一)