【问题标题】:What's faster, iterating an STL vector with vector::iterator or with at()?用 vector::iterator 或 at() 迭代 STL 向量有什么更快的方法?
【发布时间】:2010-10-21 01:39:50
【问题描述】:

在性能方面,什么会更快?有区别吗?它依赖于平台吗?

//1. Using vector<string>::iterator:
vector<string> vs = GetVector();

for(vector<string>::iterator it = vs.begin(); it != vs.end(); ++it)
{
   *it = "Am I faster?";
}

//2. Using size_t index:
for(size_t i = 0; i < vs.size(); ++i)
{
   //One option:
   vs.at(i) = "Am I faster?";
   //Another option:
   vs[i] = "Am I faster?";
}

【问题讨论】:

  • 我自己一直在做基准测试,vector.at 比使用迭代器慢得多,但是使用 vector[i] 比使用迭代器快得多。但是,您可以通过抓取指向第一个元素的指针并在当前指针小于或等于最后一个元素的指针时循环来使循环更快;类似于迭代器,但开销较小,因此在代码方面看起来不太好。这个测试是在 Windows 上使用 Visual Studio 2008 完成的。关于你的问题,我相信这取决于平台,它取决于实现。
  • 但是,从我的题外话继续我自己迭代指针,无论平台如何,都应该总是更快。
  • @leetNightshade:某些编译器在运行下标而不是指针算术时,可以使用 SIMD 指令,这会使其更快。
  • 每次循环时都在实例化结束迭代器,并且迭代器实例化不是免费的。尝试缓存您的结束迭代器。试试这个:for(vector&lt;int&gt;::iterator it = v.begin(), end= v.end(); it != end; ++it) { ... }

标签: c++ performance stl vector iterator


【解决方案1】:

使用迭代器会导致指针递增(用于递增)和取消引用会导致取消引用指针。
使用索引,递增应该同样快,但查找元素涉及添加(数据指针+索引)和取消引用该指针,但差异应该是微不足道的。
at() 还检查索引是否在界限,所以它可能会更慢。

500M 迭代的基准测试结果,向量大小 10,gcc 4.3.3 (-O3),linux 2.6.29.1 x86_64:
at(): 9158ms
operator[]: 4269ms
@987654326 @: 3914 毫秒

YMMV,但如果使用索引使代码更具可读性/可理解性,您应该这样做。

2021 年更新

使用现代编译器,所有选项实际上都是免费的,但迭代器在迭代方面稍微好一些,并且更容易与 range-for 循环一起使用 (for(auto&amp; x: vs))。

代码:

#include <vector>

void iter(std::vector<int> &vs) {
    for(std::vector<int>::iterator it = vs.begin(); it != vs.end(); ++it)
        *it = 5;
}

void index(std::vector<int> &vs) {
    for(std::size_t i = 0; i < vs.size(); ++i)
        vs[i] = 5;
}

void at(std::vector<int> &vs) {
    for(std::size_t i = 0; i < vs.size(); ++i)
        vs.at(i) = 5;
}

index()at() 生成的程序集与godbolt 相同,但iter() 的循环设置要短两个指令:

iter(std::vector<int, std::allocator<int> >&):
        mov     rax, QWORD PTR [rdi]
        mov     rdx, QWORD PTR [rdi+8]
        cmp     rax, rdx
        je      .L1
.L3:                              ; loop body
        mov     DWORD PTR [rax], 5
        add     rax, 4
        cmp     rax, rdx
        jne     .L3
.L1:
        ret
index(std::vector<int, std::allocator<int> >&):
        mov     rax, QWORD PTR [rdi]
        mov     rdx, QWORD PTR [rdi+8]
        sub     rdx, rax
        mov     rcx, rdx
        shr     rcx, 2
        je      .L6
        add     rdx, rax
.L8:                              ; loop body
        mov     DWORD PTR [rax], 5
        add     rax, 4
        cmp     rdx, rax
        jne     .L8
.L6:
        ret

【讨论】:

  • -1 抱歉。如果你看这里:velocityreviews.com/forums/…,你会发现这家伙没有使用任何编译器优化标志,所以结果基本上没有意义。
  • -1 同意 j_random_hacker - 如果您通读该主题,就会发现一些关于分析陷阱的有趣内容,以及一些更可靠的结果。
  • -1,确实如此。在不理解数字的情况下引用数字似乎是一个让 tstennner 和 bencmarker 都陷入困境的陷阱。
  • +2 现在您已经更新了更合理的衡量标准 :)
  • @Michael at() 执行边界检查,所以它是 data[i]if(i&lt;length) data[i]
【解决方案2】:

仅与原始问题略微相切,但最快的循环将是

for( size_t i=size() ; i-- ; ) { ... }

当然会倒计时。如果您的循环中有大量迭代,这确实可以节省大量资金,但它只包含少量非常快速的操作。

因此,使用 [] 运算符访问,这可能比已经发布的许多示例更快。

【讨论】:

  • 如果没有基准测试,甚至在那之后,这只是基于对机器代码的模糊概念的持久神话。几十年后倒计时不一定更快,和/或编译器在任何情况下都可以比编码器更好地优化这样的事情。 (这来自我,他经常出于反应而确实倒计时。不过,我并不认为这很重要。)如果我们仍然以 Z80 为目标,这将是相关的!
  • 错了,错了,这不是“只是一个持久的神话”,基于对机器代码的模糊概念。先生你怎么敢!事实上,我已经对此进行了基准测试,以这种方式倒计时,因为在一个步骤中将递减和评估相结合会导致更少的机器指令 - 查看汇编代码,它会更快。在我最初的帖子中,我提到只有当你有大量元素时才会看到相当大的差异,并且循环的内容非常轻量级。如果循环很大,向上或向下计数的开销就变得微不足道了。
  • 在一个循环中我们能做的很少,因为差异很重要。甚至这种差异的想法也假设人们编写了等效的循环,但是如果他们以体面的优化进行编译,那么无论如何都不会从编译器中获得免费的优化。循环的主体是什么,您使用了哪些优化设置,这在哪里“节省了大量资金”?但无论如何,归根结底,我的观点是这种事情很少值得担心,如果我们要告诉人们花时间改变他们的编码方式,他们可以研究更多更有成效的事情
  • 所以你承认这不是神话。我同意激进的优化使这些差异大多无关紧要,并且很可能最终会产生相同的代码——一个典型的例子是 ithenoob 建议的“使用后缀而不是前缀”——这 is 是一个神话:每个如果不使用返回值,即使使用 no 优化,我曾经使用过的编译器也会为这两种情况生成完全相同的机器指令。我很清楚,只有在循环体很轻的情况下,实际的循环才有意义。其他人似乎都忽略了这一事实,而您现在更新的观点似乎同意
【解决方案3】:

视情况而定。

答案比现有答案显示的要微妙得多。

at 总是比迭代器或operator[] 慢。
但对于 operator[] 与迭代器,这取决于:

  1. 你使用operator[]的准确度如何

  2. 您的特定 CPU 是否具有索引寄存器(x86 上为ESI/EDI)。

  3. 有多少其他代码也使用传递给operator[]的相同索引。
    (例如,您是否在同步索引多个数组?)

原因如下:

  1. 如果你这样做

    std::vector<unsigned char> a, b;
    for (size_t i = 0; i < n; ++i)
    {
        a[13 * i] = b[37 * i];
    }
    

    那么这段代码可能会比迭代器版本慢得多,因为它在循环的每次迭代中执行乘法操作

    同样,如果您执行以下操作:

    struct T { unsigned char a[37]; };
    std::vector<T> a;
    for (size_t i = 0; i < n; ++i)
    {
        a[i] = foo(i);
    }
    

    那么这可能会比迭代器版本慢,因为sizeof(T) 不是 2 的幂,因此你(再次)乘以 @ 987654331@每次循环!

  2. 如果您的 CPU 具有索引寄存器,那么您的代码可以使用索引而不是迭代器执行同样甚至更好的性能,如果使用索引寄存器可以释放另一个寄存器用于环形。这不是你可以通过观察来判断的;你必须分析代码和/或反汇编它。

  3. 1234563但是,如果您只迭代单个数组,那么迭代器可能会更快,因为它避免了向现有基指针添加偏移量的需要。

一般来说,您应该更喜欢迭代器而不是索引,而索引而不是指针,直到并且除非您遇到瓶颈,分析表明切换是有益的,因为迭代器是通用的-目的并且已经可能是最快的方法;它们不要求数据是可随机寻址的,这允许您在必要时交换容器。索引是下一个首选工具,因为它们仍然不需要直接访问数据——它们的失效频率较低,您可以例如用deque 替换vector 没有任何问题。指针应该是最后的手段,只有在迭代器尚未退化为发布模式下的指针时,它们才会被证明是有益的。

【讨论】:

  • 它不是索引寄存器,它像[rax + rcx*4] 一样被索引addressing modes,它允许编译器增加一个索引而不是增加多个指针。但是,它不会释放寄存器。对于每个基指针,您仍然需要一个寄存器。如果有的话,它将使用一个额外的寄存器。 (指针增量循环可能会溢出一个结束指针,并在内存中与它进行比较以获得结束条件,而不是将循环计数器保存在 reg 中。)
  • re:乘法:编译器足够聪明,可以进行强度降低优化。对于任一循环,您都应该获得 37 的增量,而不是循环计数器的乘积。在某些 CPU 上,乘法速度很慢。在现代 Intel CPU 上,imul r32, r32, imm32 是 1 uop,3c 延迟,每 1c 吞吐量一个。所以它相当便宜。如果需要多个 LEA 指令,gcc 可能应该停止将乘以小常数的乘法分解为多个指令,尤其是。使用 -mtune=haswell 或其他最新的 Intel CPU。
【解决方案4】:

这是我编写的代码,在 Code::Blocks v12.11 中编译,使用默认的 mingw 编译器。 这会创建一个巨大的向量,然后使用迭代器、at() 和索引访问每个元素。 每个通过函数调用最后一个元素循环一次,并通过将最后一个元素保存到临时内存来循环一次。

使用 GetTickCount 完成计时。

#include <iostream>
#include <windows.h>
#include <vector>
using namespace std;

int main()
{
    cout << "~~ Vector access speed test ~~" << endl << endl;
    cout << "~ Initialization ~" << endl;
    long long t;
    int a;
    vector <int> test (0);
    for (int i = 0; i < 100000000; i++)
    {
        test.push_back(i);
    }
    cout << "~ Initialization complete ~" << endl << endl;


    cout << "     iterator test: ";
    t = GetTickCount();
    for (vector<int>::iterator it = test.begin(); it < test.end(); it++)
    {
        a = *it;
    }
    cout << GetTickCount() - t << endl;



    cout << "Optimised iterator: ";
    t=GetTickCount();
    vector<int>::iterator endofv = test.end();
    for (vector<int>::iterator it = test.begin(); it < endofv; it++)
    {
        a = *it;
    }
    cout << GetTickCount() - t << endl;



    cout << "                At: ";
    t=GetTickCount();
    for (int i = 0; i < test.size(); i++)
    {
        a = test.at(i);
    }
    cout << GetTickCount() - t << endl;



    cout << "      Optimised at: ";
    t = GetTickCount();
    int endof = test.size();
    for (int i = 0; i < endof; i++)
    {
        a = test.at(i);
    }
    cout << GetTickCount() - t << endl;



    cout << "             Index: ";
    t=GetTickCount();
    for (int i = 0; i < test.size(); i++)
    {
        a = test[i];
    }
    cout << GetTickCount() - t << endl;



    cout << "   Optimised Index: ";
    t = GetTickCount();
    int endofvec = test.size();
    for (int i = 0; i < endofvec; i++)
    {
        a = test[i];
    }
    cout << GetTickCount() - t << endl;

    cin.ignore();
}

基于此,我个人认为“优化”版本比“非优化”版本更快迭代器比 vector.at() 慢,后者比直接索引慢。

我建议你自己编译并运行代码。

编辑:这段代码是我在 C/C++ 经验较少时写回的。另一个测试用例应该是使用前缀增量运算符而不是后缀。这应该会改善运行时间。

【讨论】:

    【解决方案5】:

    这真的取决于你在做什么,但是如果你必须不断地重新声明迭代器,迭代器会变得更慢。在我的测试中,最快的迭代是向你的向量数组声明一个简单的 * 并遍历它。

    例如:

    向量迭代和每次拉取两个函数。

    vector<MyTpe> avector(128);
    vector<MyTpe>::iterator B=avector.begin();
    vector<MyTpe>::iterator E=avector.end()-1;
    for(int i=0; i<1024; ++i){
     B=avector.begin();
       while(B!=E)
       {
           float t=B->GetVal(Val1,12,Val2); float h=B->GetVal(Val1,12,Val2);
        ++B;
      }}
    

    矢量点击了 90 次(0.090000 秒)

    但是如果你用指针来做......

    for(int i=0; i<1024; ++i){
    MyTpe *P=&(avector[0]);
       for(int i=0; i<avector.size(); ++i)
       {
       float t=P->GetVal(Val1,12,Val2); float h=P->GetVal(Val1,12,Val2);
       }}
    

    矢量点击 18 次(0.018000 秒)

    大致相当于...

    MyTpe Array[128];
    for(int i=0; i<1024; ++i)
    {
       for(int p=0; p<128; ++p){
        float t=Array[p].GetVal(Val1, 12, Val2); float h=Array[p].GetVal(Val2,12,Val2);
        }}
    

    数组点击了 15 次(0.015000 秒)。

    如果你取消对 avector.size() 的调用,时间就变得一样了。

    最后,用 [ ] 调用

    for(int i=0; i<1024; ++i){
       for(int i=0; i<avector.size(); ++i){
       float t=avector[i].GetVal(Val1,12,Val2); float h=avector[i].GetVal(Val1,12,Val2);
       }}
    

    矢量点击 33 次(0.033000 秒)

    用clock()计时

    【讨论】:

    • 感谢您在示例中缓存最终迭代器。
    • 第二个代码块是不是少了一个++P或者P[i]?
    【解决方案6】:

    为什么不写一个测试并找出答案?

    编辑:我的错 - 我以为我正在计时优化版本,但不是。在我的机器上,使用 g++ -O2 编译,迭代器版本比 operator[] 版本略,但可能不会明显。

    #include <vector>
    #include <iostream>
    #include <ctime>
    using namespace std;
    
    int main() {
        const int BIG = 20000000;
        vector <int> v;
        for ( int i = 0; i < BIG; i++ ) {
            v.push_back( i );
        }
    
        int now = time(0);
        cout << "start" << endl;
        int n = 0;
        for(vector<int>::iterator it = v.begin(); it != v.end(); ++it) {
            n += *it;
        }
    
        cout << time(0) - now << endl;
        now = time(0);
        for(size_t i = 0; i < v.size(); ++i) {
            n += v[i];
        }
        cout << time(0) - now << endl;
    
        return n != 0;
    }
    

    【讨论】:

    • 您是否进行了全面优化测试,并首先尝试使用迭代器版本和数组版本?性能可能略有不同,但 2 倍?没有机会。
    • 在我的测试中(使用“time” shell builtin 并且所有 cout 都被禁用并且每次都注释掉一个测试)两个版本都同样快(更改了代码,因此它在构造函数中分配,每个元素都有值“2”)。实际上,每个测试的时间变化大约 10 毫秒,我怀疑这是因为内存分配的不确定性。有时一个,有时另一个测试比另一个快 10 毫秒。
    • @litb - 是的,我怀疑我的机器上的细微差别可能是由于它的内存不足。我并不是要暗示差异很大。
    • @anon:这与更高的分辨率无关。这是关于使用clock() 而不是time() 来明确忽略“在您的代码运行时可以在现代操作系统中进行的所有其他活动”。 clock() 仅测量用于该进程的 CPU 时间。
    • 每次循环时都在实例化结束迭代器,并且迭代器实例化不是免费的。尝试缓存您的结束迭代器。试试这个:for(vector&lt;int&gt;::iterator it = v.begin(), end= v.end(); it != end; ++it) { ... }
    【解决方案7】:

    我现在在尝试优化我的 OpenGL 代码时发现了这个线程,并且想分享我的结果,即使这个线程很旧。

    背景:我有 4 个向量,大小从 6 到 12 不等。在代码开头只发生一次写入,每 0.1 毫秒对向量中的每个元素进行一次读取 p>

    以下是首先使用的代码的精简版:

    for(vector<T>::iterator it = someVector.begin(); it < someVector.end(); it++)
    {
        T a = *it;
    
        // Various other operations
    }
    

    使用此方法的帧速率约为每秒 7 帧 (fps)。

    但是,当我将代码更改为以下时,帧速率几乎翻了一番,达到 15fps。

    for(size_t index = 0; index < someVector.size(); ++index)
    {
        T a = someVector[index];
    
        // Various other operations
    }
    

    【讨论】:

    • 您是否尝试过预先增加迭代器?由于 post-inc 需要额外的复制步骤,这可能会产生影响。
    • 每次循环时都在实例化结束迭代器,并且迭代器实例化不是免费的。尝试缓存您的结束迭代器。试试这个:for(vector&lt;T&gt;::iterator it = someVector.begin(), end = someVector.end(); it != end; ++it) { ... }
    • 是的,这是一个完全不公平的测试,因为(不是个人的,而是)幼稚和草率的代码意味着它人为地削弱了迭代器的情况。
    【解决方案8】:

    您可以使用此测试代码并比较结果! 迪奥吧!

    #include <vector> 
    #include <iostream> 
    #include <ctime> 
    using namespace std;; 
    
    
    struct AAA{
        int n;
        string str;
    };
    int main() { 
        const int BIG = 5000000; 
        vector <AAA> v; 
        for ( int i = 0; i < BIG; i++ ) { 
            AAA a = {i, "aaa"};
            v.push_back( a ); 
        } 
    
        clock_t now;
        cout << "start" << endl; 
        int n = 0; 
        now = clock(); 
        for(vector<AAA>::iterator it = v.begin(); it != v.end(); ++it) { 
            n += it->n; 
        } 
       cout << clock() - now << endl; 
    
        n = 0;
        now = clock(); 
        for(size_t i = 0; i < v.size(); ++i) { 
            n += v[i].n; 
        } 
        cout << clock() - now << endl; 
    
        getchar();
        return n != 0; 
    } 
    

    【讨论】:

    • 嗯……这与 Neil 的代码并没有什么不同。为什么要发帖呢?
    • 每次循环时都在实例化结束迭代器,并且迭代器实例化不是免费的。尝试缓存您的结束迭代器。试试这个:for(vector&lt;AAA&gt;::iterator it = v.begin(), end= v.end(); it != end; ++it) { ... }
    【解决方案9】:

    差异应该可以忽略不计。 std::vector 保证其元素在内存中连续布局。因此,大多数 stl 实现将迭代器实现为 std::vector 作为普通指针。考虑到这一点,两个版本之间的唯一区别应该是第一个版本增加一个指针,而第二个版本增加一个索引,然后将其添加到指针中。所以我的猜测是第二个可能是一个非常快(就周期而言)的机器指令。

    尝试检查编译器生成的机器代码。

    不过,一般来说,如果它真的很重要,建议是进行概要分析。过早地思考这类问题通常不会给你太多回报。通常,您的代码热点位于您乍一看可能不会怀疑的其他地方。

    【讨论】:

    • 在实例化迭代器时会有明显的开销。取决于您要处理的元素数量。只要迭代器被缓存,成本应该是最小的。出于这个原因,我还建议在处理递归函数时避免使用迭代器方式。
    【解决方案10】:

    我认为唯一的答案可能是在您的平台上进行测试。通常,在 STL 中唯一标准化的是集合提供的迭代器类型和算法的复杂性。

    我会说这两个版本之间没有(差别不大)——我能想到的唯一区别是,当代码必须计算一个集合的长度时,它必须遍历整个集合数组(我不确定长度是否存储在向量内的变量中,那么开销无关紧要)

    使用“at”访问元素应该比使用 [] 直接访问它花费的时间要长一些,因为它会检查您是否在向量的范围内,如果超出范围则抛出异常(似乎 []通常只是使用指针算术 - 所以它应该更快)

    【讨论】:

      【解决方案11】:

      正如这里的其他人所说,做基准测试。

      话虽如此,我认为迭代器更快,因为 at() 也进行范围检查,即如果索引超出范围,它会引发 out_of_range 异常。该检查本身可能会产生一些开销。

      【讨论】:

        【解决方案12】:

        如果您使用的是 VisualStudio 2005 或 2008,为了从向量中获得最佳性能,您需要定义 _SECURE_SCL=0

        默认情况下 _SECURE_SCL 处于启用状态,这使得迭代包含显着变慢。也就是说,将它留在调试版本中,它将更容易追踪任何错误。需要注意的是,由于宏会更改迭代器和容器的大小,因此您必须在共享 stl 容器的所有编译单元之间保持一致。

        【讨论】:

          【解决方案13】:

          第一个在调试模式下会更快,因为索引访问会在后台创建迭代器,但在发布模式下,所有内容都应该内联,差异应该可以忽略不计或为空

          【讨论】:

          • in debug mode [...] index access creates iterators behind the scene 这对我来说将是一个巨大的[需要引用]。这是什么标准库实现?请链接到确切的代码行。
          【解决方案14】:

          如果您不需要索引,请不要使用它。迭代器概念是为您提供最好的。迭代器很容易优化,而直接访问需要一些额外的知识。

          索引用于直接访问。方括号和at 方法执行此操作。 at[] 不同,会检查越界索引,因此会更慢。

          信条是:不要要求你不需要的东西。然后编译器不会为你不使用的东西收费。

          【讨论】:

            【解决方案15】:

            我猜第一个变体更快。

            但它依赖于实现。确保您应该分析自己的代码。

            为什么要分析您自己的代码?

            因为这些因素都会改变结果:

            • 哪个操作系统
            • 哪个编译器
            • 正在使用哪种 STL 实现
            • 是否开启了优化?
            • ...(其他因素)

            【讨论】:

            • 同样非常重要:对于某些编译器和目标平台,内联 STL 容器访问的周围代码可能有利于一种方法而不是另一种方法。 (操作系统最不重要,但目标架构可能很重要)。显然,需要进行优化才能值得讨论:未优化的 STL C++ 不值得考虑。
            • 我认为你的回答解释了为什么在我自己的机器上进行分析是不够的,如果它是我将重新分发的代码 - 我需要了解它在通用机器上的作用用户,而不是它对我的作用。
            【解决方案16】:

            由于您关注的是效率,您应该意识到以下变体可能更有效:

            //1. Using vector<string>::iterator:
            
            vector<string> vs = GetVector();
            for(vector<string>::iterator it = vs.begin(), end = vs.end(); it != end; ++it)
            {
               //...
            }
            
            //2. Using size_t index:
            
            vector<string> vs = GetVector();
            for(size_t i = 0, size = vs.size(); i != size; ++i)
            {
               //...
            }
            

            因为 end/size 函数只在循环中被调用一次,而不是每次都被调用。无论如何,编译器很可能会内联这些函数,但这种方式可以确保。

            【讨论】:

            • 问题不在于如何编写高效的代码,而在于迭代器与索引,但感谢您的输入
            • 终于!关于如何正确描述这一点的正确答案。
            • @GalGoldman 不幸的是,如果您不缓存最终迭代器,那么迭代器方式相对于[] 方式具有不公平的劣势。迭代器的实例化成本很高。这也是我在使用迭代器时倾向于使用 while 循环而不是 for 循环的原因。它迫使我缓存我的迭代器。
            • @mchiasson 为什么使用while 循环“强制您缓存迭代器”?使用这种循环的一种天真的方法是 auto it = vector.begin(); while ( it++ != vector.end() ) WatchMeNotCacheAnyIterators(); 问题仍然存在:用户有责任不要编写稍短但效率可能低得多的代码。
            • @underscore_d 为真。我不知道我 2 年前在想什么,哈哈。
            猜你喜欢
            • 1970-01-01
            • 2012-11-14
            • 2020-07-23
            • 1970-01-01
            • 2020-04-18
            • 2019-08-31
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多