重新审视了QList与QVector

时间:2015-11-09 12:45:04

标签: c++ qt qt5 qlist qvector

我的问题基本上是何时选择QVector以及何时选择QList作为您的Qt容器。我所知道的:

  1. Qt docs:QList class
  2.   

    对于大多数用途,QList是正确使用的类。它的基于索引的API比QLinkedList的基于迭代器的API更方便,并且它通常比QVector更快,因为它将其项目存储在内存中。它还扩展为可执行文件中较少的代码。

    1. 这是非常受欢迎的Q& A:QVector vs QList。它也有利于QList。

    2. 但是:在最近的2015年Qt世界峰会上,KDAB提出了“为什么QList有害”,这基本上就在这里:

    3. QList considered harmful

      Don't use QList, use Q_DECLARE_TYPEINFO

      据我所知,这个想法是几乎所有类型的QList在堆中分配新元素时效率低下。每次添加新元素时,它都会调用new(每个元素一次),与QVector相比效率低。

      这就是我现在试图理解的原因:我们应该选择QVector作为默认容器吗?

8 个答案:

答案 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类别,你将无法迭代该列表
  • 当对象大于指针时,
  • 执行额外的副本和堆分配,如果对象标识很重要,这也可能是一个问题
  • 使用额外的间接访问来访问大于指针的对象
  • 由于最后两个而导致CPU时间和内存使用开销,也减少了缓存友好性
  • 在用作&#34;搜索&#34;时会带来额外的开销。返回值,因为您不太可能预先添加或甚至附加到
  • 只有在必须进行索引访问时才有意义,为了获得最佳的前置和插入性能,链表可能是更好的选择。

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_TYPEQ_DECLARE_TYPEINFO

他们引用此article by Marc Mutz

所以官方的观点已经改变了。

答案 2 :(得分:3)

QListvoid*的数组。

在正常操作中,new是堆上的元素,并在void*数组中存储指向它们的指针。与链接列表一样,这意味着列表中包含的元素的引用(但是,与链接列表不同,不是迭代器!)在所有容器修改之前保持有效,直到元素再次从容器中删除。因此名称“列表”。这个数据结构称为数组列表,用于许多编程语言,其中每个对象都是引用类型(比如Java)。它是一个非常缓存不友好的数据结构,就像所有基于节点的容器一样。

但是,可以将数组列表的大小调整纳入与类型无关的辅助类(QListData),这应该可以节省一些可执行的代码大小。在我的实验中,几乎不可能预测QListQVectorstd::vector中哪些产生的可执行代码最少。

这对于许多Qt类似引用的类型来说是一个很好的数据类型,例如QStringQByteArray等,它们只包含一个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插入缓存行,QListQList只管理 8

当文档开始调用Vector一个好的默认容器时,任何比例都出了问题。不是。 original STL states

  

std::vector是最简单的STL容器类,在很多情况下效率最高。

Scott Meyer的 Effective STL 有几个以“首选QVector结束...”开头的项目。

一般来说,C ++的真实情况并不是因为你使用的是Qt。

Qt 6将解决该特定设计错误。在此期间,请使用std::vectorIn [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 MutzOlivier Goffart视频中找到所有这些详细信息及其他内容。