就时空复杂度而言,以下哪一项是迭代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; ... */
答案 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)
除非您通过对相关代码进行概要分析发现瓶颈,否则效率(不是“效率”而是指效率)不是您的首要考虑,至少不是在此级别的代码上。更重要的是代码的可读性和可维护性!因此,您应该选择效果最好的循环变体,通常是方法4。
如果步数大于1(无论何时需要...),则索引可能会有用:
for(size_t i = 0; i < v.size(); i += 2) { ... }
尽管+= 2
本身在迭代器上也是合法的,但是如果向量的大小为奇数,则可能会在循环结束时冒未定义行为的风险,因为您会增加一个超过结束位置的值! (通常说:如果将 n 加1,如果大小不是 n 的精确倍数,则得到UB。)因此,当您不这样做时,需要附加代码来捕获没有索引变体...
答案 2 :(得分:10)
虽然对于vector
或array
而言, 1 两种形式都应该没有区别,但是习惯使用其他容器。
1 当然,只要您使用[]
而不是.at()
来按索引访问。
由于以下两个原因,在每次迭代中重新计算结束边界的效率都很低:
您可以单线操作:
for (auto it = vec.begin(), end = vec.end(); it != end; ++it) { ... }
(这是一般禁止一次声明一个变量的例外。)
for-each循环表单将自动:
因此:
for (/*...*/ value : vec) { ... }
按值获取元素和按引用获取元素之间存在明显的权衡:
在极端情况下,选择应该显而易见:
int
,std::int64_t
,void*
,...)应按值获取。std::string
,...)应该作为参考。在中间,或者当遇到通用代码时,我建议从引用开始:避免性能下降比尝试挤出最后一个循环更好。
因此,一般形式为:
for (auto& element : vec) { ... }
如果您正在处理内置的:
for (int element : vec) { ... }
1 这是优化的一般原理,实际上:局部变量比指针/引用更友好,因为优化器知道所有潜在的别名(或缺少别名)。本地变量。
答案 3 :(得分:3)
懒惰的答案:复杂性是等效的。
各种解决方案中涉及的恒定因素是实施细节。如果您需要数字,那么最好在特定目标系统上对不同解决方案进行基准测试。
存储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优化。
您可以在此处看到证明:
您会注意到,只有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 < b
和a < b-1
不同东西。)