我的问题基本上是何时选择QVector
以及何时选择QList
作为您的Qt容器。我所知道的:
对于大多数用途,QList是正确使用的类。它的基于索引的API比QLinkedList的基于迭代器的API更方便,并且它通常比QVector更快,因为它将其项目存储在内存中。它还扩展为可执行文件中较少的代码。
这是非常受欢迎的Q& A:QVector vs QList。它也有利于QList。
但是:在最近的2015年Qt世界峰会上,KDAB提出了“为什么QList有害”,这基本上就在这里:
Don't use QList, use Q_DECLARE_TYPEINFO
据我所知,这个想法是几乎所有类型的QList
在堆中分配新元素时效率低下。每次添加新元素时,它都会调用new
(每个元素一次),与QVector
相比效率低。
这就是我现在试图理解的原因:我们应该选择QVector
作为默认容器吗?
答案 0 :(得分:22)
Qt宣传QList
作为所有交易的#34;杰克,而另一半的说法是"无人的主人#34;。我说QList
是一个很好的候选者,如果你计划附加到列表的两端,并且那些不大于指针,因为QList
保留前后空间。这是关于它的,我的意思是尽可能使用QList
。
QList
会自动存储&#34;大&#34;对象作为指针并在堆上分配对象,如果你是一个婴儿,这可能被认为是一件好事,它不知道如何声明QVector<T*>
并使用动态分配。这不一定是好事,在某些情况下,它只会膨胀内存使用量并增加额外的间接性。 IMO总是一个好主意,明确你想要什么,无论是指针还是实例。即使你确实需要堆分配,最好自己分配它,只需将指针添加到列表中,而不是构造对象一次,然后在堆上有复制构造。
Qt会在很多地方为您提供QList
,例如在获得QObject
个孩子或您搜索孩子时。在这种情况下,使用在第一个元素之前分配空间的容器是没有意义的,因为它是已经存在的对象列表,而不是您可能要添加的对象。我也不喜欢缺少resize()
方法。
想象一下,在64位系统上有一个大小为9字节且字节对齐的对象。它太多了#34;对于QList
而言,它将使用8字节指针+ CPU开销来进行堆分配的慢堆分配和内存开销。它将使用两倍的内存和额外的间接访问,它几乎不会提供广告宣传的性能优势。
为什么QVector
不能突然成为&#34;默认&#34;容器 - 你不会在比赛中改变马匹 - 这是一个传统的东西,Qt是一个如此古老的框架,即使很多东西已被弃用,但是对于广泛使用的默认设置进行更改并不总是可行的,不是不会破坏大量代码或产生不良行为。无论好坏,QList
都可能在整个Qt 5中继续成为默认值,并且可能在下一个主要版本中也是如此。同样的原因Qt将继续使用&#34; dumb&#34;指针,多年来智能指针成为必须之后,每个人都在抱怨普通指针有多糟糕以及它们应该如何使用。
话虽如此,没有人强迫你在你的设计中使用QList
。没有理由QVector
不应该是您的默认容器。我自己不会在任何地方使用QList
,而在返回QList
的Qt函数中,我只是将其用作临时移动到QVector
。
此外,这只是我个人的观点,但我确实在Qt中发现了很多必须有意义的设计决策,明智的是性能或内存使用效率或易用性,总体而言很多框架和语言都喜欢推广他们的做事方式,不是因为这是最好的方式,而是因为这是他们的方式。
最后但并非最不重要:
对于大多数用途,QList是正确使用的类。
这真的归结为你如何理解这一点。在这种背景下的IMO,&#34;正确的&#34;并不代表最好的&#34;或者&#34;最佳&#34;,但对于&#34;足够好&#34;就像在&#34;它会做,即使不是最好的&#34;。特别是如果你对不同的容器类及其工作原理一无所知。
对于大多数用途,QList会这样做。
总结一下:
QList
PROs
QVector
和明确的指针来实现相同和更便宜 - 没有因为在调整列表大小时,不会移动任何对象,只会移动指针 QList
CONs
resize()
方法,reserve()
是一个微妙的陷阱,因为它不会增加有效列表大小,即使索引访问有效,它也属于UB类别,你将无法迭代该列表 CON略微超过了PRO,这意味着在&#34;休闲&#34;使用QList
可能是可以接受的,你绝对不想在CPU时间和/或内存使用是一个关键因素的情况下使用它。总而言之,QList
最适合懒惰和不小心使用,当您不想考虑用例的最佳存储容器时,通常是QVector<T>
,一个QVector<T*>
或QLinkedList
(我排除&#34; STL&#34;容器,因为我们在这里谈论Qt,Qt容器同样便携,有时更快,而且肯定更容易和更清洁使用,而std
容器是不必要的冗长的。)
答案 1 :(得分:11)
在Qt 5.7中,有关此处讨论的主题的文档已更改。在QVector中,现在说明了:
QVector
应该是您默认的首选。QVector<T>
通常会提供比QList<T>
更好的性能,因为QVector<T>
始终将其项目按顺序存储在内存中,QList<T>
将在堆上分配其项目,除非sizeof(T)
&lt; =sizeof(void*)
和T
已使用Q_MOVABLE_TYPE
声明为Q_PRIMITIVE_TYPE
或Q_DECLARE_TYPEINFO
。
他们引用此article by Marc Mutz。
所以官方的观点已经改变了。
答案 2 :(得分:3)
QList
是void*
的数组。
在正常操作中,new
是堆上的元素,并在void*
数组中存储指向它们的指针。与链接列表一样,这意味着列表中包含的元素的引用(但是,与链接列表不同,不是迭代器!)在所有容器修改之前保持有效,直到元素再次从容器中删除。因此名称“列表”。这个数据结构称为数组列表,用于许多编程语言,其中每个对象都是引用类型(比如Java)。它是一个非常缓存不友好的数据结构,就像所有基于节点的容器一样。
但是,可以将数组列表的大小调整纳入与类型无关的辅助类(QListData
),这应该可以节省一些可执行的代码大小。在我的实验中,几乎不可能预测QList
,QVector
或std::vector
中哪些产生的可执行代码最少。
这对于许多Qt类似引用的类型来说是一个很好的数据类型,例如QString
,QByteArray
等,它们只包含一个pimpl指针。对于这些类型,QList
获得了一个重要的优化:当类型不大于指针时(请注意,此定义取决于平台的指针大小 - 32或64位),而不是堆分配对象,对象直接存储在void*
个插槽中。
但这是唯一可行的,如果类型是可重定位的。这意味着它可以使用memcpy
在内存中重新定位。这里的重定位意味着我将一个对象memcpy
带到另一个地址,并且 - 至关重要 - 不运行旧对象的析构函数。
这就是事情开始出错的地方。因为与Java不同,在C ++中,对象的引用是地址。而在原始QList
中,引用是稳定的,直到将对象从集合中再次移除,方法是将它们放入此属性不再成立的void*
数组中。这不再是所有意图和目的的“列表”。
但事情仍然存在,因为它们允许严格小于void*
的类型也被放置在QList
中。但是内存管理代码需要指针大小的元素,因此QList
添加了填充(!)。这意味着64位平台上的QList<bool>
看起来像这样:
[ | | | | | | | [ | | | | | | | [ ...
[b| padding [b| padding [b...
QVector
只管理 8 ,而不是将64个bool插入缓存行,QList
,QList
只管理 8 。
当文档开始调用Vector
一个好的默认容器时,任何比例都出了问题。不是。 original STL states:
std::vector
是最简单的STL容器类,在很多情况下效率最高。
Scott Meyer的 Effective STL 有几个以“首选QVector
结束...”开头的项目。
一般来说,C ++的真实情况并不是因为你使用的是Qt。
Qt 6将解决该特定设计错误。在此期间,请使用std::vector
或In [102]: letters = list(string.ascii_lowercase[:13])
In [103]: import string
In [104]: letters = list(string.ascii_lowercase[:13])
In [105]: N = 1000
In [106]: df = pd.DataFrame({'a': np.random.choice(letters, size=N),
'b': np.random.choice(letters, size=N),
'c': np.random.choice(letters, N),
'd': np.random.randn(N)})
。
答案 3 :(得分:2)
如果QList元素类型的大小大于指针的大小 大小QList比QVector表现更好,因为它没有存储 对象按顺序存储,但按顺序存储指向堆副本的指针。
我倾向于说相反的话。在浏览物品时,情况会更糟。 如果它将它作为指针存储在堆上,那么QList会比QVector更糟糕吗?顺序存储(QVector始终如此)非常好的原因是缓存友好,一旦存储指针,就会丢失数据局部性,开始获得缓存未命中并且性能太差。
&#34;默认&#34;容器恕我直言应该是QVector(或std :: vector),如果你担心大量的重新分配,那么预先分配合理的金额,支付一次性费用,从长远来看你将受益。 / p>
默认情况下使用* Vector,如果遇到性能问题,可以根据需要进行配置和更改。
答案 4 :(得分:2)
请注意,这在 Qt6 中已经完全改变了: https://www.qt.io/blog/qlist-changes-in-qt-6
QVector 和 QList 是统一的,以 QVector 的模型作为底层实现。这意味着 Qt 5 QList 对泛型类型的额外间接级别现在消失了,元素总是直接存储在分配的内存中。 QList 是真正的类,有实现,而 QVector 只是 QList 的别名。 Qt 6 中的 QList 支持优化前置。它现在可以在不使用预留的情况下缩小元素删除。并且取消了 2GB 的大小限制。
答案 5 :(得分:0)
QList是最常用的容器,通常用作文档说明。如果元素的大小&#39; type是&lt; =指针的大小=机器&amp;操作系统位数= 4或8字节,然后对象的存储方式与QVector相同 - 按顺序存储在内存中。如果QList的元素类型的大小大于指针的大小,则QList比QVector执行得更好,因为它不会顺序存储对象,而是按顺序存储指向堆副本的指针。 在32位情况下,图片如下:
sizeof( T ) <= sizeof( void* )
=====
QList< T > = [1][1][1][1][1]
or
[2][2][2][2][2]
or
[3][3][3][3][3]
or
[4][4][4][4][4] = new T[];
sizeof( T ) > sizeof( void* )
=====
QList< T > = [4][4][4][4][4] = new T*[]; // 4 = pointer's size
| | ... |
new T new T new T
如果您希望对象在内存中按顺序布局,无论其元素的大小如何,通常是OpenGL编程的情况,那么您应该使用QVector。
这是QList内部的detailed description。
答案 6 :(得分:0)
想象一下,我们有DataType类。
QVector - 对象数组,例如:
// QVector<DataType> internal structure
DataType* pArray = new DataType[100];
QList - 指向对象的指针数组,例如:
// QList<DataType> internal structure
DataType** pPointersArray = new DataType*[100];
因此,QVector对索引的直接访问会更快:
{
// ...
cout << pArray[index]; //fast
cout << *pPointersArray[index]; //slow, need additional operation for dereferencing
// ...
}
但是,如果sizeof(DataType)&gt;,则QList的交换速度会更快。的sizeof(数据类型*):
{
// QVector swaping
DataType copy = pArray[index];
pArray[index] = pArray[index + 1];
pArray[index + 1] = copy; // copy object
// QList swaping
DataType* pCopy = pPointersArray [index];
pPointersArray[index] = pPointersArray [index + 1];
pPointersArray[index + 1] = pCopy; // copy pointer
// ...
}
因此,如果您需要直接访问而不在元素之间交换操作(例如排序)或sizeof(DataType)&lt; = sizeof(DataType *),那么更好的方法是使用QVector。在其他情况下使用QList。
答案 7 :(得分:0)
QList的行为取决于内部的内容(请参阅source code struct MemoryLayout
):
如果sizeof T == sizeof void*
和T
定义为Q_MOVABLE_TYPE
,则QList<T>
的行为与QVector
完全相同,即数据连续存储在存储器中。
如果定义了sizeof T < sizeof void*
和T
Q_MOVABLE_TYPE
,那么QList<T>
将每个条目填充到sizeof void*
,并且与QVector失去布局兼容性。
在所有其他情况下,QList<T>
是一个链接列表,因此在某种程度上会缓慢。
这种行为使得QList<T>
几乎总是一个糟糕的选择,因为依赖于漂亮的细节,QList<T>
要么是列表,要么是向量。这是糟糕的API设计,容易出错。 (例如,如果您的公共接口在内部和公共接口中使用QList<MyType>
,那么您将遇到错误。sizeof MyType is < sizeof void*
但是您忘记将MyType声明为{{1稍后,您要添加Q_MOVABLE_TYPE
。这是二进制不兼容的,这意味着您现在必须重新编译使用您的库的所有代码,因为公共API中Q_MOVABLE_TYPE
的内存布局已更改。如果你不小心,你会错过这个并引入一个错误。这很好地说明了为什么QList在这里是一个糟糕的选择。)
也就是说,QList仍然不是很糟糕:它可能会做你想要的大多数情况,但也许它会在幕后的工作方式与你的期望不同。
经验法则是:使用QList<MyType>
或QVector<T>
代替QList,因为它明确说出了您想要的内容。您可以将其与QVector<T*>
。
在C ++ 11及更高版本中,最好只使用std::unique_ptr
,因为它在range-based for loop中表现正常。 (QVector和QList可能会分离,因此执行深层复制)。
您可以在presentation from Marc Mutz和Olivier Goffart视频中找到所有这些详细信息及其他内容。