title: c++_benchMark_vector_list_deque
date: 2015-08-01 22:32:39
作者:titer1 + ZhangYu
出处:www.drysaltery.com + csdn博客同步
联系:1307316一九六八(仅接受短信)
声明:本文采用以下协议进行授权: 自由转载-非商用-非衍生-保持署名|Creative Commons BY-NC-ND 3.0 ,转载请注明作者及出处。
翻译来源:c++ benmark series
c++_benchMark_vector_list_deque
上周,我在不同的工作负载下 比较了std :: vector和std ::vector 的基准。这之前的文章收到了很多用于改进的意见和建议本文章是对前一篇文章的进一步展开。
在这篇文章中,我们将在几个不同的工作负载下,利用不同的数据类型,比较 std :: vector、std :: list和std :: deque的表现。这篇文章中,我所称呼的list、vector和deque都是标准库的对应函数
按常识来说,当实行随机的插入和删除的时候,应该使用链表,因为此时对于数组和双向队列来说,他们的相关复杂度的是O(n),而前者则是O(1)如果我们只看复杂性,在这两个数据结构(vector/deque)线性搜索的规模应该是等价的,复杂性为O(n)之中。当数组(vector)或双端队列(deque)中进行随机插入/替换操作时,所有的后续数据需要被移动,因此各元素也将被复制。这就是在进行比较这两个数据结构时为什么数据类型的大小是一个重要的因素。因为数据类型的大小将在复制元素的开销上发挥了重要作用。
然而,在实践中,对于存储器高速缓存的使用存在着巨大的差异。所有数组(vector)数据是连续的,而所在的列表(list)则为每个元素单独分配内存。这个在实际当中是如何的结果,我们将拭目以待。双端队列是针对具有以上两个数据结构的优点没有他们缺点的数据结构,我们将看到它如何在实践中如何表现。复杂度分析并没有考虑到存储器的层次结构(memory hierarchy)我相信,在实践中,存储器层级和复杂度分析一样重要。
请记住,这里的所有测试都是基于数据(vector)、链表(list)和双向队列(deque),即使其他的数据结构在给予的负载下有更好的表现。
在以下图和正文中,n是指集合的元素数。
所有的测试,都是基于 Intel Core i7 Q 820 @ 1.73GHz. 代码使用gcc在64位环境下编译,使用参数是 -02 和 -march=native. 哦,代码也支持c++ 11 的标准。
对于每一个曲线图中,垂直轴表示来执行操作的的必要时间,所以值越小越好。横轴始终是集合的元素数。对于一些图形,对数刻度可以更清晰,图下方提供一个按钮,用于改变每个图的垂直刻度为对应的对数刻度。
类型不同大小的数据,他们持有多头的数组,数组的大小变化来改变数据类型的大小。那些非标准的数据类型,在不平凡的数据类型是由两个多头,具有非常愚蠢的赋值操作符和拷贝构造函数,只是做了一些数学(完全没有意义的,但昂贵的)。可能有人会认为是不是一个普通的拷贝构造函数,既没有共同的赋值操作符和一个将是正确的,但是,这里最重要的一点是,它是昂贵的运营商这是足以让这个基准。
填充操作
所执行的第一个测试是填充数据结构,具体就是通过将元素添加(用push_back)到容器的尾部。这里将会使用两种数组(vector), 其中vector_pre是一种使用vector:reserve在一开始就分配内存的标准vector,它分配的结果是整个过程中近分配内存一次。
首先让我们看看,使用一个非常小的数据类型(8字节)填充的结果
预分配的数组fill是最快的,与vector/deque之间有很小的差距,然后list比其他三种类型慢3倍。
如果我们考虑更大的大小类型(4k):
这次vector 和list 表现相似,deque比list/vector快一点,预分配的数组明显是赢家,deque和vector之间的差异很有可能是我系统引起的,在我的机器上在同一时间不可能分配那么多内存。 (此段翻译等待代码来解释)
最后,我们将会尝试一个非普通的类型:
所有的数据表现差异不大,其中 vector_pre是最快的。
对于push_back操作, 预分配的vector是一个很好的选择如果事先知道分配的大小。。其他数据结构表现差异不大。
我更喜欢预分配的数组( Pre-allocated),如果某些人发现这些小差距的原因,请告诉我,我非常有兴趣。
线性查找
第一个操作将会在查找中测试,这个容器将会填充0到N之间的随机数字。其中,可以使用std:find来进行简单的线性查找来查询0到N的每一个数字。理论上,根据复杂度,所有的数据结构应该表现一致。
8 bytes
明显,list在查找方面表现很差,他的时间开销的增长比vector/deque都大。
128 bytes
唯一的原因是由于缓冲行的使用。当一个数据被访问时,这个数据将会从主存取到缓冲。不仅仅是访问的数据被取到,还有一整个缓冲行被取到。 由于vector中的数据都是连续的,当一访问其中的一个数据,临近的元素将自动放到cache中。因为主存的访问速度慢缓存一个数量级,因此产生巨大的差异。对于链表的情况,处理器花费所有的时间等待数据从主存取到缓冲中,对于每一次取数据操作,处理器都取到大量几乎无用的不必要的数据。
双向队列deque比vector慢一点,这是逻辑上说的过去,因为分段(segmented part)的存在,导致更多的缓存命不中。
下面尝试更大的数据类型,
链表比其他类型慢很多,但是有趣的是 双向队列和数组之间的间歇在变小,下一步我们尝试4KB
大小的数据类型。
4096 bytes
链表的表现 依旧表现很差,但是与其他之间的差距在变小。有趣点是双向队列比vector更快,
我不太确定这个结果发生的原因,很有可能是这个特别大小的数据大小。一件事是确定的,
数据单元的尺寸越大,处理器发生的缓冲行命不中的机会越多,因为元素在缓冲中没有。
对于查找来说,链表是明显的慢,同时deque和vector有类似的表现,看起来deque比vector在
大尺寸的查找操作而言更快。
随机查找(+线性查找)
8 bytes
32 bytes
128bytes
4096bytes
non-trival 16bytes
随机删除
8
http://drysaltery.qiniudn.com/2015-08-02_002644.png
128
http://drysaltery.qiniudn.com/2015-08-02_002658.png
4096
http://drysaltery.qiniudn.com/2015-08-02_002710.png
non-trival 16bytes
http://drysaltery.qiniudn.com/2015-08-02_002723.png
前插
8bytes
http://drysaltery.qiniudn.com/2015-08-02_002823.png
排序
8bytes
http://drysaltery.qiniudn.com/2015-08-02_002859.png
128bytes
http://drysaltery.qiniudn.com/2015-08-02_002908.png
1024 bytes
not 4096bytes here
http://drysaltery.qiniudn.com/2015-08-02_002923.png
non-trival 16bytes
http://drysaltery.qiniudn.com/2015-08-02_002945.png
销毁操作
8 bytes
http://drysaltery.qiniudn.com/2015-08-02_003055.png
128 bytes
http://drysaltery.qiniudn.com/2015-08-02_003105.png
4096 bytes
http://drysaltery.qiniudn.com/2015-08-02_003121.png
…
这一次,我们可以看出,deque 比vector 慢三倍。并且list 比vector 慢三个数量级!但是,性能差异在减少
当用数据结构进行测试:
list 和deque的性能没有多大区别。vector是仍然比他们快两倍,。
虽然vector始终快于即使list 和 deque,但是这是由于析构函数代价很小,微秒便可运行完。这个只影响那些对时间非常敏感的程序,然而大多程序对时间那么不敏感。此外,每个数据结构只析构一次,所以析构函数并不是一种很重要的操作。
数字运算(Number Crunching)
最后,我们将测试Number Crunching。这里,随机元素被插入有序容器。意思是,在插入之前,会先搜索出插入的位置。,这里的number crunching中测试的元素为8个字节。
即使只有100,000元素,list 已经比其他两个数据结构慢一个数量级。结果曲线显示,数据量越大,list表现越糟糕。list的空间局部性太差, 所以并不适合number crunching操作。
结论
最后,对于之前的数据结构,我们可以得到以下事实:
- std ::list 因为极差的空间局部性(spatial locality) , 使得list进行集合的遍历过程十分缓慢。
- std :: vector和std :: deque 对小数据来说 性能总是比std::list要好
- std ::list 适用于处理大型数据结构
- std::deque 在随机插入 方面 要比std::vector 好,特别是插入时有大量的push_front。
- std :: deque的和std ::vector 对复杂的数据类型支持不好, 因为复制和赋值的代价太高
我们可以得出使用这些数据结构的理想情形:
- Number crunching:使用std :: vector 或std :: deque
- 线性搜索(Linear search):使用std ::向量或std :: deque
- 随机插入/删除(Random Insert/Remove):
- 小型数据:使用std ::vector
- 大型数据:使用std ::list 只要没有大量的搜索操作。
- 复杂数据类型(对象):使用std ::list,只要不需要进行大量的搜索。但,当我们要对容器修改多次,list 将会变得很慢。
- 前插:使用std :: deque或std ::list
我坦白,在写本文之前,我并不知道的std :: deque。但,std::deque 是一个非常好的数据结构。不管是将元素插入到队列之前、之后还是中间,std::deque 都有很好的性能,并且拥有很好的空间局部性(spatial locality)。所以, 即使有时性能比vector要差,总体上可以说我们优先选择deque而不是vector 。 尤其是操作中等大小的数据结构的时候。
如果你有时间,在实践中,决定最好的办法是始终基准每个版本,甚至可以尝试另一种数据结构。两个操作具有相同大O的复杂性在实践中完全不同的表演。
我希望本文对你有所帮助。如果您有任何意见或建议。请不要犹豫,立即发表评论。
源代码: https://github.com/wichtounet/articles/blob/master/src/vector_list/bench.cpp