深入研究 STL Deque 容器

深入研究 STL Deque 容器
翻译 masterlee
 
本文档深入分析了 std::deque ,并提供了一个指导思想:当考虑到内存分配和执行性能的时候,使用 std::deque 要比 std::vector 好。
 
介绍
本文深入地研究了 std::deque 容器。本文将讨论在一些情况下使用 deque vector 更好。读完这篇文章后读者应该能够理解在容量增长的过程中 deque vector 在内存分配和性能的不同表现。由于 deque vector 的用法很相似,读者可以参考 vector 的文档中介绍如何使用 STL 容器。
 
Deque 总览
deque vector 一样都是标准模板库中的内容, deque 是双端队列,在接口上和 vector 非常相似,在许多操作的地方可以直接替换。假如读者已经能够有效地使用 vector 容器,下面提供 deque 的成员函数和操作,进行对比参考。
 
Deque 成员函数
函数
描述
c.assign(beg,end)
c.assign(n,elem)
[beg; end) 区间中的数据赋值给 c
n elem 的拷贝赋值给 c
c.at(idx)
传回索引 idx 所指的数据,如果 idx 越界,抛出 out_of_range
c.back()
传回最后一个数据,不检查这个数据是否存在。
c.begin()
传回迭代器重的可一个数据。
c.clear()
移除容器中所有数据。
deque<Elem> c
deque<Elem> c1(c2)
Deque<Elem> c(n)
Deque<Elem> c(n, elem)
Deque<Elem> c(beg,end)
c.~deque<Elem>()
创建一个空的 deque
复制一个 deque
创建一个 deque ,含有 n 个数据,数据均已缺省构造产生
创建一个含有 n elem 拷贝的 deque
创建一个以 [beg;end) 区间的 deque
销毁所有数据,释放内存。
c.empty()
判断容器是否为空。
c.end()
指向迭代器中的最后一个数据地址。
c.erase(pos)
c.erase(beg,end)
删除 pos 位置的数据,传回下一个数据的位置。
删除 [beg,end) 区间的数据,传回下一个数据的位置
c.front()
传回地一个数据。
get_allocator
使用构造函数返回一个拷贝。
c.insert(pos,elem)
c.insert(pos,n,elem)
c.insert(pos,beg,end)
pos 位置插入一个 elem 拷贝,传回新数据位置。
pos 位置插入 n elem 数据。无返回值。
pos 位置插入在 [beg,end) 区间的数据。无返回值。
c.max_size()
返回容器中最大数据的数量。
c.pop_back()
删除最后一个数据。
c.pop_front()
删除头部数据。
c.push_back(elem)
在尾部加入一个数据。
c.push_front(elem)
在头部插入一个数据。
c.rbegin()
传回一个逆向队列的第一个数据。
c.rend()
传回一个逆向队列的最后一个数据的下一个位置。
c.resize(num)
重新指定队列的长度。
c.size()
返回容器中实际数据的个数。
C1.swap(c2)
Swap(c1,c2)
c1 c2 元素互换。
同上操作。
 
Deque 操作
函数
描述
operator []
返回容器中指定位置的一个引用。
 
上面这些特征和 vector 明显相似,所以我们会提出下面的疑问。
 
问题:如果 deque vector 可以提供相同功能的时候,我们使用哪一个更好呢?
回答:如果你要问的话,就使用 vector 吧。
或者你给个解释?
非常高兴你这样问,的确,这并不是无中生有的,事实上,在 C++ 标准里解释了这个问题,在 23.1.1 章节有下面一个片断:
vector 在默认情况下是典型的使用序列的方法,对于 deque ,当使用插入删除操作的时候是一个更好的选择。
有趣的是,本文就是要非常彻底地理解这句话。      
 
什么是新的?
细读上面两张表格,你会发现和 vector 比较这里增加了两个函数。
1 c.push_front(elem) —— 在头部插入一个数据。
2 c.pop_front() —— 删除头部数据。
调用方法和 c.push_back(elem) c.pop_back() 相同,这些将来会告诉我们对于 deque 会非常有用, deque 可以在前后加入数据。
 
缺少了什么?
同时你也会发现相对于 vector 缺少了两个函数,你将了解到 deque 不需要它们。
1、   capacity() —— 返回 vector 当前的容量。
2、   reserve() —— 给指定大小的 vector 分配空间。
这里是我们真正研究的开始,这里说明 deque vector 它们在管理内部存储的时候是完全不同的。 deque 是大块大块地分配内存,每次插入固定数量的数据。 vector 是就近分配内存(这可能不是一个坏的事情)。但我们应该关注是, vector 每次增加的内存足够大的时候,在当前的内存不够的情况。下面的实验来验证 deque 不需要 capacity() reserve() 是非常有道理的。
 
实验一 —— 增长的容器
目的
目的是通过实验来观察 deque vector 在容量增长的时候有什么不同。用图形来说明它们在分配内存和执行效率上的不同。
 
描述
这个实验的测试程序是从一个文件中读取文本内容,每行作为一个数据使用 push_back 插入到 deque vector 中,通过多次读取文件来实现插入大量的数据,下面这个类就是为了测试这个内容:
# include <deque>
# include <fstream>
# include <string>
# include <vector>
 
static enum modes
{
    FM_INVALID = 0,
    FM_VECTOR, 
    FM_DEQUE   
};   
 
class CVectorDequeTest 
{   
  public :
      CVectorDequeTest();   
     
      void ReadTestFile( const char * szFile, int iMode)   
      {       
          char buff[0xFFFF] = {0};
          std::ifstream    inFile;
          inFile.open(szFile);
         
          while (!inFile.eof())
          {
              inFile.getline(buff, sizeof(buff));
             
              if (iMode == FM_VECTOR)
                      m_vData.push_back(buff);
              else if (iMode == FM_DEQUE)
                      m_dData.push_back(buff);
          }       
         
          inFile.close();
         
       }   
      
       virtual ~CVectorDequeTest();
 
  protected :   
      std::vector<std::string> m_vData;   
      std::deque<std::string> m_dData;
 };
 
结果
测试程序运行的平台和一些条件:
CPU
1.8 GHz Pentium 4
内存
1.50 GB
操作系统
W2K-SP4
文件中的行数
9874
平均每行字母个数
1755.85
读文件的次数
45
总共插入的数据个数
444330
      
使用 Windows 任务管理器来记录执行效率,本程序中使用了 Laurent Guinnard CDuration  类。消耗系统资源如下图:
 

注意在 vector 分配内存的最高峰, vector 在分配内存的时候是怎样达到最高值, deque 就是这样的,它在插入数据的同时,内存直线增长,首先 deque 的这种内存分配单元进行回收的话,存在意想不到的后果,我们希望它的分配内存看上去和 vector 一样,通过上面的测试我们需要进一步的测试,现提出一个假设:假设 deque 分配的内存不是连续的,一定需要释放和收回内存,我们将这些假设加入后面的测试中,但是首先让我们从执行的性能外表分析一下这个实验。
         究竟分配内存需要消耗多久?
         注意看下面这张图片, vector 在不插入数据的时候在进行寻求分配更多内存。

同时我们也注意到使用 push_back 插入一组数据消耗的时间,注意,在这里每插入一组数据代表着 9874 个串,平均每个串的长度是 1755.85

实验二—— vector::reserve()的资源
目的
      这个实验的目的是 vector 在加入大量数据之前调用 reserve() ,和 deque 进行比较,看它们的内存分配和执行效率怎么样?
 
描述
      本实验中的测试基本上和实验一相同,除了在测试类的构造函数中加入下面这行代码 :

m_vData.reserve(1000000);

 
结果
测试程序运行的平台和一些条件:
CPU
1.8 GHz Pentium 4
内存
1.50 GB
操作系统
W2K-SP4
文件中的行数
9874
平均每行字母个数
1755.85
读文件的次数
70
总共插入的数据个数
691180
使用 Windows 任务管理器来记录执行效率,本程序中使用了 Laurent Guinnard CDuration   类。消耗系统资源如下图:


我们注意到 vector 不在需要分配花费多余的时间分配内存了,这是由于我们使用了 reserve() 对于所测试的 691180 个数据为我们每一次插入大量数据的时候保留了足够的内存空间,对于 deque 存储分配的假设,观察这个测试中的内存分配图形和上一个图形,我们需要进一步量化这个测试。
怎样改良内存分配的性能呢?
下面这个图例说明随着数据的增加,容量在增加:
当增加数据的时候对容量的增加在 vector deque 执行效率基本一样,然而, vector 在插入数据的时候有一些零星的时间消耗,看下面的图例:


通过统计分析 vector deque 在插入平均为 1755.85 长度的 9874 个数据所花费的时间,下面是总结的表格:

Vector
Deque
Mean
0.603724814 sec
Maximum
0.738313000 sec
Minimum
0.559959000 sec
Std. Dev
0.037795736 sec
6-Sigma
0.226774416 sec
 
Mean
0.588021114 sec
Maximum
0.615617000 sec
Minimum
0.567503000 sec
Std. Dev
0.009907800 sec
6-Sigma
0.059446800 sec
 

 
实验三——内存回收
目的
本实验是对假设 deque 分配的内存不是临近的,而且很难回收进行量化测试分析。
 
描述
在本实验中再次用到了实验一中的代码,在调用函数中加入记录增加数据执行的效率具体入下面操作:

for (xRun=0; xRun<NUMBER_OF_XRUNS; xRun++)
    {
        df = new CVectorDequeTest;
 
        elapsed_time = 0;
        for (i=0; i<NUMBER_OF_RUNS*xRun; i++)
        {
            cout << "Deque - Run " << i << " of " <<
                            NUMBER_OF_RUNS*xRun << "... ";
            df->ReadTestFile("F://huge.csv",DF_DEQUE);
 
            deque_data.push_back(datapoint());
 
            deque_data.back().time_to_read = df->GetProcessTime();
            elapsed_time += deque_data.back().time_to_read;
 
            deque_data.back().elapsed_time = elapsed_time;
 
            cout << deque_data.back().time_to_read << " seconds/n";
        }
 
        vnElements.push_back(df->GetDequeSize());
 
        cout << "/n/nDeleting... ";
 
        del_deque.Start();
        delete df;
        del_deque.Stop();
 
        cout << del_deque.GetDuration()/1000000.0 << " seconds./n/n";
 
        vTimeToDelete.push_back(del_deque.GetDuration()/1000000.0);
    }

 
结果
本测试和上面两个实验在相同的平台上运行,除了插入的数据由 9874 691180 ,需要插入 70 次,下面图例显示了 deque 在插入数据的时候分配内存的情况,在 deque 里插入了平均每个长度为 1755.85 的字符串。
尽管从几个曲线图中看到的实际消耗时间不同,但些曲线图都精确到了 R2=95.15% 。所给的数据点都实际背离了下表中统计的曲线图数据 :
deque Results
Mean
0.007089269 sec
Maximum
11.02838496 sec
Minimum
-15.25901667 sec
Std. Dev
3.3803636 sec
6-Sigma
20.2821816 sec
 
在相同的情况下比较 vector 的结果是非常有意义的。下面图就是将 vector deque 在相同的情况下分配内存消耗的时间比较图:

这些数据在这个测试中是 R2=82.12% 。这或许可以经过每个点反复运行得到更加优化,在这个问题中这些数据适当地标注了这些点,所给的数据点都实际背离了下表中统计的曲线图数据 :
vector Results
Mean
-0.007122715 sec
Maximum
 0.283452127 sec
Minimum
-0.26724459 sec
Std. Dev
0.144572356 sec
6-Sigma
0.867434136 sec
 
实验四—— vector::insert() deque::insert() 执行特点比较
目的
      deque 主张使用参数为常量的 insert() 。但怎么样能和 vector::insert() 比较一下呢?本实验的目的就是比较一下 vector::insert() deque::insert() 的工作特点。      
 
描述
      在容器的容器多次插入数据,在这里可能不符合你的需求,既然这样你可以使用 insert() ,试验代码也和实验一基本一样,使用 insert() 代替 push_back() ,使用 insert( ) 来测试。
 
结果
      当插入常量给 deque 的时候,从下图可以看出和 vector 的对比来。

注意两张图片中时间轴的不同,这是将 61810 个数据插入到容器中。

实验五——读取容器的性能
目的
      这个实验将测试 vector::at(),vector::operator[],deque::at() deque::operator[] 的性能。首先应该是 operator[] at () 效率要高,因为它不进行边界检查,同时也比较 vector deque
 
描述
      这个实验将测试中的容器有 1000000 个类型为 std::string ,每个字符串长度为 1024 的数据,分别使用 at() operator[] 这两个操作来访问容器容器的数据,测试它们运行的时间,这个测试执行 50 次,统计每次执行的结果。
 
结果
我们看到使用 vector deque 访问容器中的数据,他们执行的性能差别很小,使用 operator[] at() 访问数据的性能差别几乎可以忽略不计,下面是统计的结果:

vector::at()
Mean
1.177088125 sec
Maximum
1.189580000 sec
Minimum
1.168340000 sec
Std. Dev
0.006495193 sec
6-Sigma
0.038971158 sec
 
deque::at()
Mean
1.182364375 sec
Maximum
1.226860000 sec
Minimum
1.161270000 sec
Std. Dev
0.016362148 sec
6-Sigma
0.098172888 sec
 
vector::operator[]
Mean
1.164221042 sec
Maximum
1.192550000 sec
Minimum
1.155690000 sec
Std. Dev
0.007698520 sec
6-Sigma
0.046191120 sec
 
deque::operator[]
Mean
1.181507292 sec
Maximum
1.218540000 sec
Minimum
1.162710000 sec
Std. Dev
0.010275712 sec
6-Sigma
0.061654272 sec
 

 
结论
在这篇文章中我们覆盖了多种不同的情况来选择我们到底是该使用 vector 还是 deque 。让我们总结一下测试的结果看下面几个结论。
 
当执行大数据量的调用push_back()的时候,记住要调用vector::reserve()
      在实验一中我们研究了 vector deque 在插入数据的情况。通过这些假设,我们可以看出 deque 分配的空间是预先分配好的, deque 维持一个固定增长率,在 vector 实验中我们考虑到应该调用 vecor::reserve() . 然后在下面这个例子验证了我们的假设,在使用 vector 的时候调用 reserve() 能够膀子我们预先分配空间,这将是 vector 一个默认选择的操作。
 
当你分配很多内存单元的时候,记住使用deque回收内存要比vector消耗时间多。
      在实验三中我们探讨了 vector deque 在回收非邻接内存块上的不同,分别证明了 vector 在分配内存的时候是线性增长,而 deque 是指数增长,同样, vector 要回收的内存比 deque 多的多,如果你循环调用了 push_back() ,那么 deque 将获取大量的内存,而且是临近的。我们通过测试发现在分配内存单元消耗的时间和 vector 的时间接近。
 
如果你计划使用insert(),或者需要pop_front(),那就使用deque
      由于 vector 没有提供 pop_front() 函数,但在实验四的结果中可以看出没有 insert() 是非常好的 同时也告诉我们为什么 deque STL 类中要作为单独的一个类划分出来。
 
对于访问数据,vector::at()效率最高。
在实验五中统计的数据表示,所有访问数据方法的效率是非常接近的,但是 vector::at() 效率最高。这是因为最优的平衡图访问时间为最低的六个西格玛值。
 
最后
我希望本文能够带你认识 deque ,而且对它感兴趣或者一个启发,欢迎继续讨论关于 vector deque 任何问题和内容。       
 
参考文献
Plauger, P.J. Standard C++ Library Reference. February, 2003. MSDN.
ISO/IEC 14882:1998(E). Programming Languages - C++. ISO and ANSI C++ Standard.
Schildt, Herbert. C++ from the Ground Up, Second Edition. Berkeley: 1998.
Sutter, Herb. More Exceptional C++. Indianapolis: 2002.
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值