“默认情况下,C++标准库提供了合理的性能”。如果你对“合理的”一词暗含的意思有过好奇,请接着读下去……

引言

假设我们希望从一个文件中将一串类型为double的值读进一个数据结构中,从而允许我们高效地访问这些值,通常的方法如下:
vector<double> values;
double x;
while (cin >> x)values.push_back(x);  
当循环结束时,values会容纳有所有的值,我们将可以通过values[i]高效地访问任何值。  在直觉上,标准库vector类就像一个内建数组:我们可以认为它在单块连续的内存中容纳其元素。实际上,尽管C++标准没有明确要求vector的元素要占用连续的内存,然而标准委员会在2000年10月份的会议上裁定此项要求的遗漏归因于工作上的疏忽,并且投票表决将其作为技术勘误的一部分而包含进来。这个迟到的要求谈不上是多大的痛苦,因为每一个现有的vector实现本来就是以这种方式工作的。  如果一个vector的元素位于连续的内存中,我们就很容易明白它是如何高效地访问个体元素的 — 只要使用与内建数组相同的机制就可以了。不过,要弄明白一个vector实现是如何处理高效增长的问题就不是这么简单了,因为这种增长将不可避免地涉及到将元素从一块内存区域拷贝到另外一块内存区域。尽管现代处理器通常特别擅长于将一块连续的数据从内存的一个地方拷贝到另一个地方,然而这样的拷贝并非是免费的午餐。因此,思考一个标准库实现可能是如何处理vector的增长而又不消耗过量的时间或空间,很有意义。  本文的余下部分将讨论一个用于管理vector增长的简单而高效的策略。

尺寸和容量

要想搞清楚vector类的工作机制,首先要清楚它并不仅仅是一块内存。相反,每一个vector都关联有两个“尺寸”:一个称为尺寸(size),表示vector容纳的元素的数量;另一个称为容量(capacity),表示可被用来存储元素的内存总量。在vector尾部留有额外的内存的用意在于,当使用push_back向vector追加元素时无需分配更多的内存。如果邻接于vector尾部的内存当时恰好未被占用,那么vector的增长只要将那块内存合并过来即可。然而这样的好运气极其罕见,大多数情况下需要分配新的内存,然后将vector现有的元素拷贝到那块内存中,然后销毁原来的元素,最后归还元素先前占用的内存。在vector中留有额外的内存的好处在于,这样的重新分配(代价可能很昂贵)不会每当试图向vector追加一个元素时都发生。

重新分配内存的代价有多高昂?它涉及如下四个步骤:

•为需要的新容量分配足够的内存; •将元素从原来的内存拷贝到新内存中; •销毁原来的内存中的元素; •归还原来的内存。  如果元素的数目为n,那么我们知道步骤(2)和(3)都要占用O(n)的时间,除非分配或归还内存的代价的增长超过O(n),否则这两步将在全部运行时间中占居支配地位。因此我们可以得出结论:无论用于重新分配的容量(capacity)是多少,重新分配一个尺寸(size)为n的vector需要占用O(n)的时间。  这个结论暗示了一种折衷权衡。假如在重新分配时请求大量的额外内存,那么在相当长的时间内将无需再次进行重新分配,因此总体重新分配操作消耗的时间相对较少,这种策略的代价在于将会浪费大量的空间。另一方面,我们可以只请求一点点额外的内存,这么做将会节约空间,但后继的重新分配操作将会耗费时间。换句话说,我们面临一个经典的抉择:拿时间换空间,或者相反。

重新分配策略

作为一个极端的例子,假定每当填充vector一次我们就将其容量增加1个单位,这种策略耗费尽可能少的内存空间,但每当追加一个元素时都要重新分配整个vector。我们说过,重新分配一个具有n个元素的vector占用O(n)的时间,因此,如果我们从一个空vector开始并将其增长到k个元素,那么占用的总时间将会是O(1+2+...+k)或者O(k2),这太可怕了!有没有更好的办法呢?  比方说,假如不是以1个步幅增长vector的容量,而是以一个常量C的步幅来增长它将会如何?很明显这个策略将会减少重新分配的次数(基于因子C),所以这当然是一种改进,但这个改进到底有多大呢?  理解这个改进的方式之一是要认识到此一新策略将针对每C个元素块进行一次重新分配。假设我们为总量为KxC个元素分配K块内存,那么,第一次重新分配将会拷贝C个元素,第二次将会拷贝2xC个元素,等等。Big-O表示法不考虑常量因子,因此我们可以将所有的C因子分摊开来而获得O(1+2+...+K)或者O(K2)的总时间。换句话说,时间仍然是元素个数的二次方程,不过是带有一个小得多的因子罢了。  撇开较小的因子不谈,“二次行为”仍然太糟糕,即使有一个快速的处理器也是如此。实际上,对于快速的处理器来说尤其糟糕,因为快速的处理器通常伴有大量的内存,而访问具有大量内存的快速处理器的程序员常常试图用尽那些内存(这是迟早的事)。这些程序员往往会发现,如果在运行一个二次算法的话,处理器的速度于事无补。  我们刚刚证实,一个希望能以小于“二次时间”而分配大型vector的实现是不能使用“每次填充时以常量步幅增长vector容量”的策略的,相反,被分配的附加内存的数量必须随着vector的增长而增长。这个事实暗示存在一种简单的策略:vector从单个元素开始而后每当重新分配时倍增其容量,如何?事实证明这种策略允许我们以O(n)的时间构建一个有着n个元素的vector。  为了理解是如何获得这样的效率的,考虑当我们已经完全填满它并打算对其重新分配时的vector的状态:  自最近一次重新分配内存以来被追加到vector中的元素有一半从未被拷贝过,而对于那些被拷贝的元素而言,其中一半只被拷贝了一次,其余的一半被拷贝了两次,以此类推。  换句话说,有n/2的元素被拷贝了一次或多次,有n/4的元素被拷贝了两次或多次,等等。因此,拷贝元素的总数目为n/2 + n/4 +...,结果可以近似为n(随着n的增大,这个近似值越发精确)。撇开拷贝动作不谈,有n个元素被追加到了vector中,但操作占用的时间总量仍然是O(n)而不是O(n2)。

讨论

C++标准并没有规定vector类必须以某种特定的方式管理其内存,它只是要求通过重复调用push_back而创建一个具有n个元素的vector耗费的时间不得超过O(n),我们刚才讨论的策略可能是满足此项要求的最直截了当的一种。  因为对于这样的操作来说vector具有优秀的时间性能,所以没有什么理由避免使用如下循环:
vector<double> values;
double x;
while (cin >> x)values.push_back(x);
是的,当其增长时,实现将会重新分配vector的元素,但是,如果我们事先能够预测vector最终尺寸的话,这个重新分配耗费的时间将不会超过“一个常量因子”可能会占用的时间。

练习

1.设想我们通过以如下方式编写代码而努力使我们那个小型循环速度更快:
while (cin >> x)
{if (values.size() == values.capacity())values.reserve(values.size() + 1000);values.push_back(x);
}
效果将会如何?成员函数reserve进行一次重新分配,从而改变vector的capacity,使其大于或等于其参数。  2.设想不是每次倍增vector的尺寸,而是增大三倍,在性能上将会产生什么样的影响?特别是,创建一个具有n个元素的vector的运行时间仍然为O(n)吗?  3.设想你知道你的vector最终将拥有多少元素,在这种情况下,在填充元素之前你可以调用reserve来预先分配数量合适的内存。试一试你手边的vector实现,看看调用reserve与否对你的程序的运行时间有多大的影响。

附STL源码:

template <class T, class Alloc = alloc>
class vector{
public:typedef T value_type;typedef value_type* iterator;protected:iterator start; //  指向头结点    iterator finish; // 尾结点    iterator end_of_storage; //容量public:// 获取容量函数size_type capacity() const{return size_type(end_of_storage - begin());}// 默认构造函数,可见都被初始化为0vector() : start(0), finish(0), end_of_storage(0) {}// 带容量的构造函数explicit vector(size_type n) { fill_initialize(n, T()); }// fill_initialize实现如下    void fill_initialize(size_type n, const T& value){start = allocate_and_fill(n, value);finish = start + n;end_of_storage = finish; // 可见容量被设置为n的值    }// push_back()调用insert搜索_aux函数    void insert_aux(iterator position, const T& x){if (finish != end_of_storage)    //    else{const size_type old_size = size();const size_type len = old_size != 0 ? 2 * old_size : 1;    // 可见,初始为0,添加一个后容量变1,以后空间不够的话,新容量为原先2倍    }}
};  

关于vector的容量增长问题相关推荐

  1. C++ STL vector的容量

    关于vector的容量: vs:如果容量不够时,增加现有容量的一半(向下取增): vc6.0:如果容量不够时,增加现有容量的一倍: 关于vector的大小: size()为vector中元素的个数,和 ...

  2. [转贴]从零开始学C++之STL(二):实现一个简单容器模板类Vec(模仿VC6.0 中 vector 的实现、vector 的容量capacity 增长问题)...

    首先,vector 在VC 2008 中的实现比较复杂,虽然vector 的声明跟VC6.0 是一致的,如下: C++ Code  1 2   template < class _Ty, cla ...

  3. Linux一个cpu有多少个vector,C++中vector容器大小增长规律浅析

    问:"vector大小是如何增长的?" 答:"自动增长的" 问:"增长规律是?" 答: ...... 那么, 今天就来探究一下vector容 ...

  4. C++vector容器-容量和大小

    vector容量和大小 功能描述: 对vector容器的容量和大小操作 函数原型: 代码如下: #include <iostream> using namespace std; #incl ...

  5. c++:vector对象的增长

    容器大小管理操作 容器大小操作函数 c.shrink_to_fit() 请将capacity()减小为与size()相同大小 c.capacity() 不重新分配内存空间的话,c可以保存多少元素 c. ...

  6. Java ArrayList、LinkedList和Vector的使用及性能分析

    第1部分 List概括 List 是一个接口,它继承于Collection的接口.它代表着有序的队列. AbstractList 是一个抽象类,它继承于AbstractCollection.Abstr ...

  7. 深入研究 C++中的 STL Deque 容器

    本文档深入分析了std::deque,并提供了一个指导思想:当考虑到内存分配和执行性能的时候,使用std::deque要比std::vector好. 介绍 本文深入地研究了std::deque 容器. ...

  8. LIST函数JAVA特点_Java 集合系列 07 List总结(LinkedList, ArrayList等使用场景和性能分析)...

    java 集合系列目录: 第1部分 List概括 先回顾一下List的框架图 (01) List 是一个接口,它继承于Collection的接口.它代表着有序的队列. (02) AbstractLis ...

  9. 优秀互联网面试题总结

    ------------------------------------------- 公司面试回忆 ------------------------------------------------- ...

最新文章

  1. 小调查:足足两周了,下周你上班否?
  2. 正确入门Service Mesh:起源、发展和现状
  3. Elasticsearch之如何合理分配索引分片
  4. mysql7种join连接_mysql 重新整理——七种连接join连接[六]
  5. IBastis配置实例
  6. Spring Boot filter
  7. 明日之后服务器维修会补偿什么,明日之后:服务器修复后官方发来补偿,玩家居然怀疑奖励不真实?...
  8. oracle导出建表主键,oracle主键自动生成 配合hibernate的生成策略详解
  9. Lambda、函数式接口、Stream 一次性全给你
  10. 两个类相互包含对方成员的问题(2)
  11. CSS webkit
  12. python画拓扑图权值是线条粗细_python—networkx:根据图的权重画图
  13. 搭建 WPF 上的 UI 自动化测试框架
  14. uni-app调用百度OCR身份证识别的api,实现身份证文字识别
  15. HTML之我的个人主页
  16. iOS 指纹识别/人脸识别登录(ECDSA 加签)
  17. Windows 10 C盘大瘦身
  18. [Klipper从入门到放弃]香橙派zero2设置2.4g无线热点
  19. MySQL:带你掌握表的增删查改
  20. byte java 详解_详解java中的byte类型

热门文章

  1. 穿越梦想 起锚远航──金旭亮新作《.NET 4.0面向对象编程漫谈》
  2. 【Spring Boot】闲聊Spring Boot(一)
  3. 大学毕业十年后(转)
  4. Linux学习笔记(三)源码编译OpeCVPCL
  5. QCustomPlot 的使用-折线图和散点图
  6. Census Transform
  7. 朗强科技:HDMI画面分割器原理、功能、分割模式、应用
  8. 45本免费的JavaScript书籍资源收集
  9. anki for android,Anki-Android
  10. 2021年国家补助政策怎么领取