迭代std :: vector的最有效方法是什么?为什么?

时间:2019-04-10 06:36:10

标签: c++ performance stl iterator

就时空复杂度而言,以下哪一项是迭代std :: vector的最佳方法,为什么?

方法1:

for(std::vector<T>::iterator it = v.begin(); it != v.end(); ++it) {
    /* std::cout << *it; ... */
}

方法2:

for(std::vector<int>::size_type i = 0; i != v.size(); i++) {
    /* std::cout << v[i]; ... */
}

方法3:

for(size_t i = 0; i != v.size(); i++) {
    /* std::cout << v[i]; ... */
}

方法4:

for(auto const& value: a) {
     /* std::cout << value; ... */

9 个答案:

答案 0 :(得分:38)

首先,方法2 方法3 在几乎所有标准库实现中都是相同的。

除此之外,您发布的选项几乎等效。唯一显着的区别是,在方法1 方法2/3 中,您依靠编译器来优化对v.end()v.size()的调用出来。如果这个假设是正确的,则循环之间没有性能差异。

如果不是,方法4 效率最高。回想一下基于范围的for循环如何扩展到

{
   auto && __range = range_expression ;
   auto __begin = begin_expr ;
   auto __end = end_expr ;
   for ( ; __begin != __end; ++__begin) {
      range_declaration = *__begin;
      loop_statement
   }
}

这里的重要部分是,这保证end_expr仅被评估一次。另外请注意,要使基于范围的for循环成为最有效的迭代,您不得更改处理迭代器取消引用的方式,例如

for (auto value: a) { /* ... */ }

这会将向量的每个元素复制到循环变量value中,该循环变量可能比for (const auto& value : a)慢,具体取决于向量中元素的大小。

请注意,使用C ++ 17中的并行算法功能,您还可以尝试

#include <algorithm>
#include <execution>

std::for_each(std::par_unseq, a.cbegin(), a.cend(),
   [](const auto& e) { /* do stuff... */ });

但是这是否比普通循环更快取决于具体情况。

答案 1 :(得分:11)

添加到lubgranswer

除非您通过对相关代码进行概要分析发现瓶颈,否则效率(不是“效率”而是指效率)不是您的首要考虑,至少不是在此级别的代码上。更重要的是代码的可读性和可维护性!因此,您应该选择效果最好的循环变体,通常是方法4。

如果步数大于1(无论何时需要...),则索引可能会有用:

for(size_t i = 0; i < v.size(); i += 2) { ... }

尽管+= 2本身在迭代器上也是合法的,但是如果向量的大小为奇数,则可能会在循环结束时冒未定义行为的风险,因为您会增加一个超过结束位置的值! (通常说:如果将 n 加1,如果大小不是 n 的精确倍数,则得到UB。)因此,当您不这样做时,需要附加代码来捕获没有索引变体...

答案 2 :(得分:10)

优先使用迭代器而不是索引/键。

虽然对于vectorarray而言, 1 两种形式都应该没有区别,但是习惯使用其他容器。

1 当然,只要您使用[]而不是.at()来按索引访问。


记住终点。

由于以下两个原因,在每次迭代中重新计算结束边界的效率都很低:

  • 通常:本地变量没有别名,这对优化程序更友好。
  • 在除矢量以外的其他容器上:计算末端/尺寸可能会更昂贵。

您可以单线操作:

for (auto it = vec.begin(), end = vec.end(); it != end; ++it) { ... }

(这是一般禁止一次声明一个变量的例外。)


使用for-each循环形式。

for-each循环表单将自动:

  • 使用迭代器。
  • 记住底限。

因此:

for (/*...*/ value : vec) { ... }

按值获取内置类型,按引用获取其他类型。

按值获取元素和按引用获取元素之间存在明显的权衡:

  • 通过引用获取元素可以避免复制,这可能是一项昂贵的操作。
  • 按值获取元素更易于优化, 1

在极端情况下,选择应该显而易见:

  • 内置类型(intstd::int64_tvoid*,...)应按值获取。
  • 可能分配的类型(std::string,...)应该作为参考。

在中间,或者当遇到通用代码时,我建议从引用开始:避免性能下降比尝试挤出最后一个循环更好。

因此,一般形式为:

for (auto& element : vec) { ... }

如果您正在处理内置的:

for (int element : vec) { ... }

1 这是优化的一般原理,实际上:局部变量比指针/引用更友好,因为优化器知道所有潜在的别名(或缺少别名)。本地变量。

答案 3 :(得分:3)

懒惰的答案:复杂性是等效的。

  • 所有解决方案的时间复杂度为Θ(n)。
  • 所有解的空间复杂度为Θ(1)。

各种解决方案中涉及的恒定因素是实施细节。如果您需要数字,那么最好在特定目标系统上对不同解决方案进行基准测试。

存储v.size() rsp可能会有所帮助。 v.end(),尽管这些通常是内联的,所以可能不需要这样的优化,或者performed automatically

请注意,索引(没有记忆v.size())是正确处理可能添加(使用push_back()的附加元素的循环体的唯一方法。但是,大多数用例不需要这种额外的灵活性。

答案 4 :(得分:1)

为完整起见,我想提一下您的循环可能想要更改向量的大小。

std::vector<int> v = get_some_data();
for (std::size_t i=0; i<v.size(); ++i)
{
    int x = some_function(v[i]);
    if(x) v.push_back(x);
}

在这样的示例中,您必须使用索引,并且每次迭代都必须重新评估v.size()

如果对基于范围的for循环或迭代器执行相同操作,则可能会出现未定义行为,因为向向量中添加新元素可能会使迭代器无效。

顺便说一句,相对于while循环,我更喜欢在这种情况下使用for循环,但这是另一回事。

答案 5 :(得分:1)

这在很大程度上取决于您所说的“有效”。

其他答案都提到了效率,但我将重点介绍C ++代码最重要的(IMO)目的:向其他程序员传达您的意图¹。

从这个角度来看,方法4显然是最有效的。不仅因为要读取的字符较少,而且主要是因为认知负荷较少:我们不需要检查边界或步长是否异常,循环迭代变量({{1 }}或i)可以在其他任何地方使用或修改,无论是否有错字或复制/粘贴错误,例如it,或其他数十种可能性。

快速测验:给定for (auto i = 0u; i < v1.size(); ++i) { std::cout << v2[i]; },以下多少个循环是正确的?

std::vector<int> v1, v2, v3;

尽可能清晰地表达循环控制,使开发人员可以更深入地了解高级逻辑,而不会为实现细节所困扰-毕竟,这就是为什么我们首先使用C ++的原因!


¹明确地说,当我编写代码时,我认为最重要的“其他程序员”是Future Me,试图理解,“ 谁写了这个垃圾?” ...

答案 6 :(得分:1)

建议使用方法4,std :: for_each(如果确实需要)或方法5/6:

void method5(std::vector<float>& v) {
    for(std::vector<float>::iterator it = v.begin(), e = v.end(); it != e; ++it) {
        *it *= *it; 
    }
}
void method6(std::vector<float>& v) {
    auto ptr = v.data();
    for(std::size_t i = 0, n = v.size(); i != n; i++) {
        ptr[i] *= ptr[i]; 
    }
}

前3种方法可能会遇到指针别名(如先前答案中提到的那样)的问题,但都同样糟糕。考虑到可能有另一个线程 访问该向量,大多数编译器将对其进行安全处理,并在每次迭代中重新评估[] end()和size()。这将阻止所有SIMD优化。

您可以在此处看到证明:

https://godbolt.org/z/BchhmU

您会注意到,只有4/5/6使用vmulps SIMD指令,而1/2/3只能使用非SIMD vmulss指令。

注意:我在Godbolt链接中使用VC ++,因为它很好地演示了该问题。 gcc / clang确实会发生相同的问题,但是用Godbolt演示它并不容易-通常,您需要分解DSO才能看到这种情况。

答案 7 :(得分:0)

您列出的所有方式都具有相同的时间复杂度和相同的空间复杂度(这不足为奇)。

使用for(auto& value : v)语法效率稍高,因为使用其他方法,编译器可能会在每次执行测试时从内存中重新加载v.size()v.end(),而使用for(auto& value : v)永远不会发生(它只会加载一次begin()end()迭代器)。

我们可以在这里观察每种方法产生的装配体的比较:https://godbolt.org/z/LnJF6p

有点有趣的是,编译器将method3实现为对jmp的{​​{1}}指令。

答案 8 :(得分:0)

除了最后一个容器在理论上要快得多之外,其他所有容器的复杂性都相同,因为容器的末尾仅被评估一次。

最后一个也是最好的读写方式,但缺点是无法给您提供索引(这通常很重要)。

但是您忽略了我认为是一个很好的选择(当我需要索引并且不能使用for (auto& x : v) {...}时,这是我的首选):

for (int i=0,n=v.size(); i<n; i++) {
    ... use v[i] ...
}

请注意,我使用的是int而不是size_t,并且结尾仅计算一次,并且在主体中也可以用作局部变量。

通常,当需要索引和大小时,也会对它们进行数学计算,并且size_t在用于数学运算时表现得“奇怪”(例如a+1 < ba < b-1不同东西。)