我目前正在和一些朋友一起从事c ++学校项目。
在用c ++编写矢量之前,我做了类似的事情来使用它们:
unsigned int i = 0;
while (i != myVector.size())
{
doSomething(myVector[i]);
i++;
}
但是在这个项目期间,我的朋友不高兴看到我使用这样的向量,并要求我使用迭代器。 我不太喜欢迭代器,因为很难记住它们的语法,但是我的朋友们说使用它们会更好,因为它的工作速度更快。 而且由于我们正在处理具有大量向量的大型项目,因此使用迭代器至关重要。
时间过去了,即使我仍然不记得他们的语法,但我仍在使用它们,但我想查看迭代器方法是否真的比“ unsigned int”方法更快。
所以我做了这两个程序:
使用unsigned int方法的第一个程序:
#include <vector>
#include <string>
#include <iostream>
int main()
{
std::string str = "This is a string";
int i = 0;
std::vector<std::string> vec;
while (i != 10000000)
{
vec.push_back(str);
i++;
}
unsigned int j = 0;
while (j != vec.size())
{
std::cout << vec[j] << std::endl;
j++;
}
return (0);
}
第二个程序使用迭代器方法:
#include <vector>
#include <string>
#include <iostream>
int main()
{
std::string str = "This is a string";
int i = 0;
std::vector<std::string> vec;
while (i != 10000000)
{
vec.push_back(str);
i++;
}
std::vector<std::string>::iterator it;
it = vec.begin();
while (it != vec.end())
{
std::cout << *it << std::endl;
it++;
}
return (0);
}
如您所见,两个程序都将首先创建一个大小为1 000 000的矢量(我放了一个大尺寸,因此如果时间上的差异将更容易发现),然后我将打印出向量中使用字符串,但是使用两种不同的方法。
我在Linux上花费时间来了解每个程序的执行时间,如下所示:
time ./a.out
结果如下:
unsigned int方法:
real 0m39,391s
user 0m5,463s
sys 0m21,108s
迭代器方法:
real 0m39,436s
user 0m5,972s
sys 0m20,652s
然后.........是同一时间?! 两者之间的差异不到1秒,并且是一个包含1000万个字符串的向量。
所以我想知道这两种方法之间真的有区别吗,迭代器真的更好用吗?
答案 0 :(得分:4)
使用迭代器的主要原因不是性能,而是错误的可能性和表达性更高的可能性。比较
unsigned int i = 0;
while (i != myVector.size())
{
doSomething(myVector[i]);
i += 2;
}
或
unsigned int start = myVector.size() + 42;
for (unsigned int i = start; i != myVector.size(); ++i){
doSomething(myVector[i]);
}
使用
for (const auto& e : myVector) {
doSomething(e);
}
基于范围的for循环使迭代器的使用尽可能简单(您甚至看不到迭代器,但它们在后台使用)。当您手动管理索引时,有数百万种方法会弄错索引,而使用迭代器则可能有2或3。
为了进行性能比较:当向量将其元素存储在连续内存中时,向量迭代器可以是普通指针。您认为开销主要是语法糖,使您可以编写更好的代码。因此,您看不出太大的差别并不奇怪。
PS
我经常使用它,我有信心不会犯太多错误
根据我自己的经验,我非常了解这种感觉。不久前,我认为我在使用c样式数组的奥秘技术上已经达到了可接受的水平,并且我不愿意使用std::vector
,因为我确信它不能像原始数组一样有效。我本来没有错,但这是一个不同的故事。
使用整数来迭代数组是上世纪以来的事情。它不安全,导致难以发现错误,并且可以轻松调用未定义的行为。编写代码来表达您想要做什么,而不是指示处理器。如果要对向量的每个元素进行操作,则应使用std::for_each
:
std::for_each(myVector.begin(),myVector.end(),doSomething);
它没有手动使用索引的任何缺点(您是否发现了上述循环中的错误?),并且具有无论实际使用的是myVector
容器还是哪种容器都具有相同外观的优点。包含的元素或实际上是什么doSomething
(可以是自由函数,函子,lambda,您可以选择)。
答案 1 :(得分:3)
令人惊讶的是,在循环中将迭代器访问与索引访问进行比较时,至少在理论上可能存在性能差异,如此处所示:https://gcc.godbolt.org/z/frFYhF
使用迭代器过滤一些噪声,每次迭代看起来像
.LBB0_1: # =>This Inner Loop Header: Depth=1
movl (%rbx), %edi
callq check(int)
addq $4, %rbx
cmpq %rbx, %r14
jne .LBB0_1
因此,在这里我们看到一个内存访问,一个数学运算和一个条件分支。总体而言,一旦您超出了缓存行,内存访问将使所有其他文件相形见,但这些操作仍然是必需的。
当我们研究索引访问时,迭代看起来像:
.LBB1_3: # =>This Inner Loop Header: Depth=1
movq (%r14), %rax
movl (%rax,%rbx,4), %edi
callq check(int)
addq $1, %rbx
cmpq %r15, %rbx
jb .LBB1_3
在这里,我们看到了在上一个示例中没有看到的东西-每次迭代都会有一个额外的寄存器移动(这是位移内存访问所必需的)。
现在,寄存器移动可能是CPU可以执行的最便宜的实际操作之一,但它仍然是一个操作,并且它将是一个重新排序块,因为以后的操作取决于它的结果。
我相信,在访问引导程序时,我们在此处看到的性能影响不应是您在考虑的事情。相反,您应该追求均匀性,易于阅读和可维护性。
话虽如此,我建议您选择基于范围的循环。
for (int i: vec) {
// work with i
}
最后但不是列表,使用变量unsigned int
遍历向量的索引可能是一个讨厌的错误。在许多平台上,向量可能大于maxim int允许的值,您将最终遇到无限循环。