为什么std::list性能这么差?

为什么std::list性能这么差?

1.现象

现象1:最近在写一个新系统,方式是开发一个动态库,嵌入某个主程序中运行,主程序调用我实现的接口完成某些功能。未嵌入动态库时,主程序的TPS可以达到10万,嵌入动态库后,一开始TPS很高,但很快就降到了4000左右。后来我发现是动态库中用了一个list,而这个list的内容没有及时清空导致的。

让我疑惑的有两点:1)我其实并没有对list做显式的遍历操作,按说list的长度不应该影响程序性能的;2)list的长度只有区区4000多,而当时性能已经下降得不成样子了。

现象2:其他项目的同事也表示list有性能问题,但没有提供更多信息。

受好奇心驱使,我决定一探究竟。

先声明:由于时间限制,我这里并没有专门写测试程序进行详细测试,测试结果都是基于我在做的具体项目的具体情况简单测试的,仅说明在这种特定的场景下有这样的特定问题,以后有时间会做详细的补充测试。

2.原因

std::list的size方法,在某些平台下是用遍历实现的(而我恰巧用了大量的size()!=0来判断list是否为空,换成empty()函数会好很多)

首先获取g++的stl源代码,位于:https://github.com/gcc-mirror/gcclibstdc++-v3目录

list的size()函数实现位于:include/bits/stl_list.h

可以看到,size()函数底层调用的是std::distance()函数,而empty()函数则是直接通过指针判断里链表是否为空。

std::distance()的实现位于include/bits/stl_iterator_base_funcs.h:

该函数在底层调用了__distance函数,而这个函数有两个实现:

对于随机读取的容器(以random_access_iterator_tag标识),调用的是下面的函数,直接首尾相减即可;对于非随机读取容器(以input_iterator_tag标识),需要遍历

这几个iterator_tag的定义位于include/bits/stl_iterator_base_types.h:

这几种iterator代表的含义是:

  • input:只读迭代器
  • output:只写迭代器
  • forward:读写迭代器
  • bidirectional:双向移动迭代器
  • random:随机访问迭代器

这里用了一个巧妙的编码技巧,用struct的继承结构来表现类型从属关系,上面的继承结果表示的是:

  • random_access_iterator使用上述的第二个__distance实现,即首尾相减快速计算元素个数
  • 其他的bidirectional、forward、input都会使用第一个__distance实现,即需遍历才能计算元素个数

一些常用的列表容器重,vector提供的是random_access_iterator,支持随机访问:

而list提供的是bidirectional_iterator,支持双向访问:

3.使用建议

C++之父Bjarne Stroustrup提出,应该默认使用vector而不是list,因为vector相比list至少有如下几点好处:

  • 数据存储是相邻的,对缓存友好,命中率高
  • vector没有频繁的动态内存申请和释放,抖动较小

这里是原文:链接

这里有一个视频,在1:07:00左右,Bjarne Stroustrup就list和vector的使用提出了见解,并和一位观众进行了讨论:链接

另外,如果必须要使用list,请考虑如下建议:

  • 不要让list的元素数量太多
  • 判空时,使用empty()而非size()==0,因为在某些平台下,求size要遍历整个链表!包括GCC都是这样的实现!
  • 加上编译优化命令(比如-O3,实测可以提高40%的性能)

4.相关资源

1.gcc stl源码地址:链接

2.stackoverflow上的一个问答:链接

3.C++之父关于list的使用建议:链接

4.C++之父关于list的使用建议(视频):链接

5.侯捷《STL源码剖析》第三章 迭代器概念与traits编程技法 第3.4.5小节 迭代器相应型别之五:iterator_category

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注