Hap*_*rry 1 c++ foreach iterator x86-64
为什么是以下情况:
for (auto & x : vec) { /* do stuff with x */ }
Run Code Online (Sandbox Code Playgroud)
比...快
for (int i = 0; i < v.size(); ++i) { /* do stuff with v[i] */ }
Run Code Online (Sandbox Code Playgroud)
正如我的标题所说,我被告知增加和取消引用迭代器比增加单独的变量并将其索引到数组中更快,但不明白为什么?
究竟发生了什么让它变得更快?
我试图走过去看看发生了什么,这就是我想到的
// pseudo instructions
// iterator
increment iterator
e.g., if the object is 3 integers you would increment the iterator
by 12
dereference this iterator
// pseudo instructions
// array
increment array index
add 1 to index counter
multiply by size of object
e.g., if the object is 3 integers, you multiply by 12
dereference the array
Run Code Online (Sandbox Code Playgroud)
遍历数组似乎相当简单。您只需执行以下操作:
[offset + index * size] ; An array takes this form in assembly
Run Code Online (Sandbox Code Playgroud)
如果你有一个整数数组,看起来会像这样
[rbp - 16 + rdi * 4] ; rbp - 16 is the offset
; rdi is the index
; 4 is the size of the integer
Run Code Online (Sandbox Code Playgroud)
数组迭代似乎相当微不足道,所以我不明白取消引用迭代器如何更快。
至少在未优化的代码中,它可能更快的唯一方法是因为您size()在每次迭代开始时调用成员函数,并且它实际上取决于它使用的容器和迭代器类型。否则,对类似数组的容器使用迭代器与使用指针算术相同,哪个顺序更快取决于优化。此外,如果i 具有适当的类型大小,不会因不同宽度的值而使编译器感到困惑,这也会有所帮助。
尽管如果您正在修改同一容器,基于范围的 for 循环比基于索引的 for 循环有更多的无效问题。因为它的编码是这样的:
基于范围的 for 语句
for ( for-range-declaration : for-range-initializer ) statement
Run Code Online (Sandbox Code Playgroud)
相当于
{
auto &&__range = for-range-initializer ;
auto __begin = begin-expr ;
auto __end = end-expr ;
for ( ; __begin != __end; ++__begin ) {
for-range-declaration = *__begin;
statement
}
}
Run Code Online (Sandbox Code Playgroud)
正如您所看到的,范围开始和结束的迭代器在循环之前进行评估,并且对范围采取的任何使它们无效的操作都会调用 UB。
如果我们谈论容器的通用概念而不仅仅是容器std::vector,那么对于某些容器来说operator[]可能比迭代器增量更昂贵。只有少数标准容器具有恒定成本,operator[]并且在视图\范围的情况下,其成本通常较大、非恒定或非线性。递增迭代器的成本往往是恒定的或线性的。成本通常会被声明或可以在性能测试期间找到。
所以主要问题不是性能而是代码的正确性,并且使用基于范围的 for 循环表明范围没有改变。故意引起此类更改变得更加困难,因为您可以访问的只是元素的引用或副本。这样做的尝试是显而易见的,这将是对原始范围的一些使用。它还保存了 begin-expr/end-expr 的样板代码。
通常,for 循环调用的大小i < v.size()可能表明size()在执行期间可能会发生变化。如果这是真的并且发生在循环体的中间,您可能会发现索引从该点开始超出范围。
当在不知道代码作者的情况下审查代码时,如果我寻找此类更改的来源,则从我看到其第一行后跟一些不透明的那一刻起,每个此类循环都是检测到错误或越界访问崩溃的嫌疑人代码。
比for (auto & x : vec) { /* do stuff with x */ }快吗for (int i = 0; i < v.size(); ++i) { /* do stuff with v[i] */ }?
当你绝对地谈论更快时,你基本上总是错的。每个案例都不同。堆栈对齐的简单更改可以使代码速度提高 40%,这比一般情况下-O0的差异还要大。-O2因此,只需将局部变量添加到调用图中较早的某个函数就可以完全逆转更快的速度。
始终使用真实输入来衡量代码的性能!
也就是说,for 循环v.size()在每次迭代中都会调用。自动循环不会这样做。编译器可能会也可能不会意识到,v在循环期间, 的变化永远不会改变。如果没有,就会导致速度变慢。
但如果它同时采用两种编写方式,循环应该变成这样(在 x86_64 上):
size_t count = vec.end() - vec.begin();
T *it = vec.begin();
while (count-- > 0) {
/* do stuff with *it */
++it;
}
Run Code Online (Sandbox Code Playgroud)
这通常是经过良好优化的循环所采用的形式。或者更具体地说,在 asm a中do{}while(--count != 0),如果应该运行零次迭代,则在循环外部使用代码不进入循环。或者do{}while(++ptr < endptr)将it其自身用作循环计数器,从而避免实际计算指针减法的计数。
您认为数组访问应采用以下形式的想法[rbp - 16 + rdi * 4]是错误的。虽然手动编写是完全合理的,但生成的机器代码比使用[rdi]andadd rdi, 4每个循环一次要大。这将节省指令缓存,特别是在多次访问指针的情况下,并且最终速度更快。该> 0测试也比< end处理索引时更简单,因此如果编译器使用单独的count寄存器,它可以执行dec rcx / jnz .loop_top(在 Intel 上为 1 uop)而不是inc// cmp(jne需要第二个寄存器作为限制,并且在 Intel 上为 2 uop) Intel 和 AMD,宏融合 cmp+jcc)。
总的来说,如果两种形式中的一种比另一种更快,那么要么是优化失败(以相同的方式),要么是该v.size()调用无法消除。