首 页 | 新 闻 | 技术中心 | 第二书店 | 《程序员》 | 《开发高手》 | 社 区 | 黄 页 | 人 才
移 动专 题SUNIBM微 软微 创精 华Donews人 邮
我的技术中心 
我的分类 我的文档
全部文章 发表文章
专栏管理 使用说明



 RSS 订阅 
最新文档列表
Windows/.NET
.NET  (rss)    
Visual C++  (rss)    
Delphi  (rss)    
Visual Basic  (rss)    
ASP  (rss)    
JavaScript  (rss)    
Java/Linux
Java  (rss)    
Perl  (rss)    
综合
其他开发语言  (rss)    
文件格式  (rss)    
企业开发
游戏开发  (rss)    
网站制作技术  (rss)    
数据库
数据库开发  (rss)    
软件工程
其他  (rss)    

积极原创作者 
goodboy1881 (13)
wangchinaking (58)
iiprogram (67)
fancyhf (1)
harrymeng (41)
yjz0065 (113)
coofucoo (105)
Drate (69)
lphpc (30)
smallnest (61)
CSDN - 文档中心 - 其他开发语言 阅读:2268   评论: 0    参与评论
标题   深入研究 STL Deque 容器     选择自 masterlee 的 Blog
关键字   深入研究 STL Deque 容器
出处  

深入研究 STL Deque 容器

An In-Depth Study of the STL Deque Container (By Nitron)

翻译 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

nelem的拷贝赋值给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个数据,数据均已缺省构造产生

创建一个含有nelem拷贝的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位置插入nelem数据。无返回值。

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)

c1c2元素互换。

同上操作。

 

Deque操作

函数

描述

operator[]

返回容器中指定位置的一个引用。

 

上面这些特征和vector明显相似,所以我们会提出下面的疑问。

 

问题:如果dequevector可以提供相同功能的时候,我们使用哪一个更好呢?

回答:如果你要问的话,就使用vector吧。

或者你给个解释?

非常高兴你这样问,的确,这并不是无中生有的,事实上,在C++标准里解释了这个问题,在23.1.1章节有下面一个片断:

vector在默认情况下是典型的使用序列的方法,对于deque,当使用插入删除操作的时候是一个更好的选择。

有趣的是,本文就是要非常彻底地理解这句话。     

 

什么是新的?

细读上面两张表格,你会发现和vector比较这里增加了两个函数。

1c.push_front(elem) —— 在头部插入一个数据。

2c.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存储分配的假设,观察这个测试中的内存分配图形和上一个图形,我们需要进一步量化这个测试。

怎样改良内存分配的性能呢?

下面这个图例说明随着数据的增加,容量在增加:


当增加数据的时候对容量的增加在vectordeque执行效率基本一样,然而,vector在插入数据的时候有一些零星的时间消耗,看下面的图例:

通过统计分析vectordeque在插入平均为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);

    }

 

结果

本测试和上面两个实验在相同的平台上运行,除了插入的数据由9874691180,需要插入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的结果是非常有意义的。下面图就是将vectordeque在相同的情况下分配内存消耗的时间比较图:


这些数据在这个测试中是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()效率要高,因为它不进行边界检查,同时也比较vectordeque

 

描述

      这个实验将测试中的容器有1000000个类型为std::string,每个字符串长度为1024的数据,分别使用at()operator[]这两个操作来访问容器容器的数据,测试它们运行的时间,这个测试执行50次,统计每次执行的结果。

 

结果

我们看到使用vectordeque访问容器中的数据,他们执行的性能差别很小,使用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()

      在实验一中我们研究了vectordeque在插入数据的情况。通过这些假设,我们可以看出deque分配的空间是预先分配好的,deque维持一个固定增长率,在vector实验中我们考虑到应该调用vecor::reserve().然后在下面这个例子验证了我们的假设,在使用vector的时候调用reserve()能够膀子我们预先分配空间,这将是vector一个默认选择的操作。

 

当你分配很多内存单元的时候,记住使用deque回收内存要比vector消耗时间多。

      在实验三中我们探讨了vectordeque在回收非邻接内存块上的不同,分别证明了vector在分配内存的时候是线性增长,而deque是指数增长,同样,vector要回收的内存比deque多的多,如果你循环调用了push_back(),那么deque将获取大量的内存,而且是临近的。我们通过测试发现在分配内存单元消耗的时间和vector的时间接近。

 

如果你计划使用insert(),或者需要pop_front(),那就使用deque

      由于vector没有提供pop_front()函数,但在实验四的结果中可以看出没有insert()是非常好的同时也告诉我们为什么dequeSTL类中要作为单独的一个类划分出来。

 

对于访问数据,vector::at()效率最高。

在实验五中统计的数据表示,所有访问数据方法的效率是非常接近的,但是vector::at()效率最高。这是因为最优的平衡图访问时间为最低的六个西格玛值。

 

最后

我希望本文能够带你认识deque,而且对它感兴趣或者一个启发,欢迎继续讨论关于vectordeque任何问题和内容。      

 

参考文献

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.

 

   郑重声明:
                
允许复制、修改、传递或其它行为
                
但不准用于任何商业用途.
                 
写于  2004/11/7  masterlee


相关文章
对该文的评论