QVector和QList在实际开发中,如何选择这个问题,小生一直有这个疑问。无意间,在查阅资料时,发现了一篇分析得比较好的文章(文末有列出)。这篇文章的一些观点和数据也来源于该篇文章。
一、【QVector】解析
QVector可能是Qt中最接近STL的容器。尽管如此,QVector在许多平台上的性能还是比std::vector差,这是因为它的内部结构更复杂。通过比较GCC 4.3.2 (x86-64) -O2编译环境下,在QVector (Qt 4.6.3)以及std::vector上迭代操作部分生成的代码可知:
由以上汇编代码可知:
对于实际循环(. L11 /. L3)部分,QVector和std::vector是相同的,这表明编译器在这两种情况下都能很好识别迭代器。然而,区别在于在循环(.LCFI5/.LCFI2)之前填充%rbx (it)和%rbp (end)这部分代码:QVector的代码显然更复杂(包含了:更多的命令,更复杂的寻址模式,计算依赖于以前的结果),并且执行速度更慢。
这意味着对于空集合和小集合的迭代,QVector的开销将高于std::vector。QVector迭代生成的代码也比std::vector迭代稍微多一些。
QVector的一个优势是它的增长优化。当其元素类型为Q_MOVABLE_TYPE或Q_PRIMITIVE_TYPE时,QVector将使用realloc()来增加容量。STL容器也是这样做的,但是由于缺乏可移植的类型分类方案,它们只对针对于内置类型或不可移植的声明。
可以通过使用reserve()来消除此影响。这不仅是STL vector的一个优点,还可以加快Qt vector的速度并节省内存。
还应提到的是,由于内部函数QVector::realloc()的实现方式的影响,QVector要求元素类型提供一个默认构造函数,即使是简单的push_back()。相反,std::vector不要求元素类型为DefaultConstructible,除非调用显式要求的std::vector函数,例如std::vector(int)。
二、【QList】解析
QList是Qt容器的”白色坟墓”,“白色坟墓”,哈哈。
对于Qt来说,QList与std::list没有任何关系。QList被实现为一个数组列表,实际上是一个连续的void *s块,在前面和后面都有一点空间,允许前置(非常罕见)和追加(非常常见)操作。void *slot包含指向单个元素的指针(这些元素被复制构造到动态内存中,例如:使用new的时候),除非元素类型是可移动的,即声明为Q_MOVABLE_TYPE,并且足够小,即不大于void*,在这种情况下,元素被直接放入void*slot中。
下文将研究下这种设计的利弊:
从有利的方面来看:当针对生成的代码数量时,QList是一个真正的内存节省者。这是因为QList只是一个内部类的包装,内部类维护void*s的内存,这会使代码更紧凑,因为所有内存管理代码都是在不同类型的QLists之间共享的。
QList还允许较快的中间插入和相当有效的容器增长(只需要移动指针,而不是数据本身,所以QList总是可以使用realloc()来增长自己)操作。但是,这种效率不会比预期的QVector<void*>高多少,而QVector通常并不以中间快速插入而著称。
消极的一面是:当涉及到保存大多数数据类型时,QList存在真正的内存浪费现象。
这将会有两方面的影响:
(1)如果元素类型是可移动的并且足够小,那么当元素类型的大小小于sizeof(void*): sizeof(void*)-sizeof(T)时,QList会浪费内存。这是因为每个元素至少需要sizeof(void*)存储空间。换句话说:一个QList使用4×/8×(32/64位平台)的内存。
(2)如果元素类型不是可移动的或太大(sizeof(T) > sizeof(void*)),则将在堆上分配。因此,将为每个元素支付堆分配开销(取决于分配器,通常在0到12或2个字节之间),加上保存指针所需的4/8个字节
只有当元素类型是可移动且大小为sizeof(void*)时,QList才是一个好容器。这种情况至少发生在最重要的隐式共享Qt类型(QString, QByteArray,但也有QPen, QBrush等)中。下表描述的是Qt 4.6.3中隐式共享类型列表,以及它们是否适合作为QList的元素:
不适合QList:太大 | 不适合QList:不是Moveable的 | 适合 |
---|---|---|
QBitmap, QFont, QGradient, QImage, QPalette, QPicture, QPixmap, QSqlField, QTextBoundaryFinder, QTextFormat, QVariant(!) | QContiguousCache, QCursor, QDir, QFontInfo, QFontMetrics, QFontMetricsF, QGLColormap, QHash, QLinkedList, QList, QMap, QMultiHash, QMultiMap, QPainterPath, QPolygon, QPolygonF, QQueue, QRegion, QSet, QSqlQuery, QSqlRecord, QStack, QStringList, QTextCursor, QTextDocumentFragment, QVector, QX11Info | QBitArray, QBrush, QByteArray, QFileInfo, QIcon, QKeySequence, QLocale, QPen, QRegExp, QString, QUr |
注:QCache缺少必要的复制构造函数,所以它不能作为QList中的元素使用。
在开发中,可以使用Q_DECLARE_TYPEINFO
标记我们自己的类实例化:
Q_DECLARE_TYPEINFO( QList<MyType>, Q_MOVABLE_TYPE );
当将它们放入QList时,它们将是有效的。
然而,问题是当在放入QList时,如果忘记了使用Q_DECLARE_TYPEINFO标记该类型,那么就不能把他添加到QList中,至少不能以二进制兼容的方式添加它,因为声明一个可移动类型会改变该类型QList的内存布局。
下描述一些基本类型的内存效率。它们都是可移动的,因此作为QList元素的潜在效率很高。在表中,“大小”是元素类型的大小,“Mem / Elem”是每个元素使用的内存(以字节为单位),对于32/64-bit平台。
Type | Size | Mem/Elem | Overhead |
---|---|---|---|
bool/char | 1 | 4/8 | 300%/700% |
qint32/float | 4 | 4/8 | 0%/100% |
qint64/double | 8 | 16+/8 | 100%+/0% |
特别注意的是会出现一个类型在32位平台上高效而在64位平台上低效(例如float)的情况,反之亦然(例如double)
【实践指南】当QList中T没有声明为Q_MOVABLE_TYPE或Q_PRIMITIVE_TYPE,或者sizeof(T) != sizeof(void*)(记住检查32和64位平台)时,避免使用QList。
三、总结
因此,QList不是一个好的默认容器。但是是否存在QList优于QVector的情况?遗憾的是,答案是否定的。这也是最好的答案,如果Qt 5只是去替换所有出现的QList与QVector。QList优于QVector的几个基准要么与在实践中无关,要么应该通过优化QVector来修复。
也就是说,在编写QVector之前,应该三思,即使它的性能可能会稍微好一些。这是因为QList通常用于Qt中QString的集合,在开发中,应该避免在Qt API使用QList的地方使用QVector。
四、尾
参考资料:
【1】https://marcmutz.wordpress.com/effective-qt/containers/#containers-qlist。
【2】https://doc.qt.io/qt-5/qlist.html。
【3】https://doc.qt.io/qt-5/qvector.html
搜索关注【嵌入式小生】wx公众号获取更多精彩内容>>>>