c++ 标准库的各种容器(vector,deque,map,set,unordered_map,unordered_set,list)的性能考虑

c++ 标准库的各种容器(vector,deque,map,set,unordered_map,unordered_set,list)的性能考虑

一、vector

vector采用一段连续的内存来存储其元素,向vector添加元素的时候,如果容量不足,vector便会重新malloc一段更大的内存,然后把原内存中的数据memcpy到新的内存中,并free原内存块,然后将新元素加入。vector的元素插入性能跟以下几个要素关系重大:

1. 插入的位置

头部插入:将所有元素后移,然后将新元素插入

中间插入:将插入点后面的元素后移,然后插入新元素

尾部插入:将新元素直接插入尾部

尾部插入无疑是最快的,头部插入最慢,中间插入次之,慢的点在于插入前要移动内存。

删除元素也是同样的道理。

2. 保留空间大小

如果插入元素是,空间不足将导致重新malloc以及一系列的内存拷贝。如果使用者能对容量有预期,那么采用reserve()来预先分配内存,将大大的提高性能。

3. 内存扩展算法的库实现相关

在空间不足导致需要重新malloc的时候,不同的库实现有很大的不同,往往特定平台的实现会结合操作系统的平台特性以及malloc算法,提供相当优秀的内存扩展算法,在百万次vector<int>的插入操作中,不提供reserve()的情况向,性能表现非常优秀。接近于使用了reserve()的情况。

因为vector采用连续的内存存储其元素,因此其支持元素的下标法随机访问,且时间复杂度是常量0;

如果是查找元素,vector的find()成员函数对元素进行查找时是采用从头到尾扫描的方式,他时间复杂度是O(n),如果vector要应付查找的性能需求,那么应该采取排序的vector,利用算法库的getlowerbound()进行元素的有序插入,利用binary_search()对元素进行二分查找。这种情况下其查找性能不输于基于红黑树的set和map,更是令list望尘莫及。

综述,vector适用于尾部插入,但是此时无法兼顾查找的性能,因为二分查找的vector要求重新排序,或者要求vector在插入时就保持有序,这样就无法做到尾部插入。

但是vector作为动态数组的替代,已经足够优秀。

二、deque

deque采用多块内存串起来的方式提供其元素的存错,每一个内存块存储多个元素,每一块内存存储的元素个数相同,这是他不同于vector采用一块内存来存错所有的元素的方式。这样带来的好处是:

首先,头部插入和尾部插入/删除元素的成本是一样,弥补了vector再头部插入元素性能不佳的问题;

其次,对于vector的一个内存块的模式,当有巨大数量的元素,操作系统的大内存分配和赋值时很缓慢的,而且deque的方式就不会带来这个问题。

缺点是:

首先对元素的访问需要经过两个层次,第一次找到元素所在的内存块,第二次找到块中的元素。不过这个时间几乎是可以忽略,除非对性能要求极其苛刻。

其次,对其进行排序,以及排序后的查找会比较慢,想象知名的排序算法,都是针对一段连续内存进行下标访问,而deque是断续的内存块组成。同样排序后的折半查找也无法利用下标直接访问自然性能大打折扣。

三、list

list很简单,他就是个双向链表。每一个节点的内存都是独立的。理论上,其优点是任何位置的插入删除元素操作都是非常块的。缺点是不适合用于元素的查找,因为他只能是扫描的方式。根据实际的测试情况来看,我认为list不值得一用,因为节点的频繁新增与删除将导致大量重复的内存分配和释放操作。而实际上,操作系统以及运行库的内存分配与释放频率和策略才是影响stl各大容器的性能最关键的点。

四、map/multi_map/set/multi_set

这四个数据结构是采用平衡树(红黑树)实现其元素在内存中的存储。理论上(与list一样是理论上)他们的性能是很高,而且在插入/删除与元素查找上是的平衡点掌握的相当好的,具体的算法复杂度可参考红黑树算法文献,但是同样基于“操作系统以及运行库的内存分配与释放频率和策略才是影响stl各大容器的性能最关键的点”这一法则,他们在理论之外,实际的应用中,总是表现得不是太好。

五、散列容器unordered_map/unordered_set/unordered_multi_map/unordered_multi_set

c++11引入的散列容器,散列容器具有不稳定性:他依赖于实际所使用的散列算法。而针对不同的元素数量,不同的散列算法具有相当大的性能差异。

所以,理论上(同样是理论上)他们的算法时间复杂度是近乎“常量”(如果要处理冲突那就不是了)。但时间上对于一般用户使用起来可能会带来风险。除非你对所使用的散列算法和元素数量都有很好的预估。在我一些简单的测试(采用int元素类型,采用std的默认散列函数)中,散列容器的性能是排名垫底的。

六、“内存分配与释放频率和策略才是影响stl各大容器的性能最关键的点”

这点非常重要。不同的操作系统,不同的运行库,他的虚拟内存管理算法,以及运行库的malloc和free的内部实现都是有差异的。如何找出最有效率的使用标准库容器的方式,一定要结合这一点进行大量的测试才能得出。

有一点可以确认的是:各个标准库的实现,已经是在遵循c++标准的基础上,在性能和适用性上可以说做到了极致。因此很多人总想自己造重复的轮子是大可不必。理解好各种容器的实现原理,再结合实际应用的需求,选择合适的容器,以及容器的使用方式,才是上上之策。

发布了38 篇原创文章 · 获赞 19 · 访问量 12万+

猜你喜欢

转载自blog.csdn.net/truexf/article/details/17303263