【问题标题】:Is there an equivalent of vector::reserve() for an std::list?std::list 是否有等效于 vector::reserve() 的方法?
【发布时间】:2012-05-27 07:21:22
【问题描述】:

我有一个如下所示的课程:

typedef std::list<char*> PtrList;
class Foo
{
public:
   void DoStuff();
private:
   PtrList m_list;
   PtrList::iterator m_it;
};

函数DoStuff() 基本上是向m_list 添加元素或从中删除元素,找到其中某个特殊元素的迭代器并将其存储在m_it 中。需要注意的是,m_it 的每个值都会在随后的每个 DoStuff() 调用中使用。

那么问题出在哪里? 一切正常,除了分析显示操作员 new 被调用过多,因为 list::push_back()DoStuff() 调用。

为了提高性能,我想在 Foo 的初始化中为 m_list 预分配内存,就像它是 std::vector 一样。问题是这会引入新的问题,例如:

  1. 效率较低的 inserterase 元素。
  2. 只要向量从一个调用更改为 DoStuff() 到下一个,m_it 就会失效。 编辑: Alan Stokes 建议使用索引而不是迭代器来解决这个问题。

我的解决方案:我能想到的最简单的解决方案是实现一个对象池,该池还具有链表功能。这样我就得到了一个链表并且可以为它预分配内存。

是我遗漏了什么还是它真的是最简单的解决方案?我宁愿不“重新发明轮子”,而是使用标准解决方案(如果存在)。

任何想法、解决方法或有启发性的 cmets 都将不胜感激!

【问题讨论】:

  • 你到底在用那些 char* 做什么?
  • @ScarletAmaranth 这些是指向内存中存储数据包的位置的指针(我将其简化为 char* 问题)。
  • 你有没有检查过如果你只使用vector,它是否真的表现得更好?它可能会很好;快速分配和良好的缓存行为通常可以弥补插入/删除成本。
  • 您可以指定自定义Allocatorlist 模板的第二个参数)并使用它从您的池中分配。您可能会发现 boost::pool 很有用。
  • @Eitan 您能否将索引而不是迭代器存储到vector 中以避免失效? (插入和删除时要小心。)

标签: c++ list stl vector


【解决方案1】:

可能会使用list::get_allocator().allocate()。 Afaik,由于lists 的非连续性质,默认行为将是在何时获取内存-因此缺少reserve()-但我立即想到使用allocator 方法没有重大缺点。如果您的程序中有一个非关键部分,在开始时或其他任何情况下,您至少可以选择在那一点承受伤害。

【讨论】:

    【解决方案2】:

    我认为你使用了错误的容器。

    如果你想快速推回那么不要自动假设你需要链表,链表是一个慢速容器,它基本上适合重新排序。

    更好的容器是 std::deque。双端队列基本上是一个数组数组。它分配一块内存并在您推回时占用它,当它用完时它将分配另一个块。这意味着它只会非常不频繁地分配,并且您不必像 std::vector 和 reserver 那样提前知道容器的大小以提高效率。

    【讨论】:

    • 我需要一个快速的erase 和一个快速的push_back
    • 您是否真的将 erasestd::dequestd::list 的基准进行了对比。 std::list 与连续存储容器相比确实非常慢。你的套装有多大。
    • 没有对它进行基准测试,没有。我的集合将包含大约 10,000 个元素。
    • 我会建议对它进行基准测试,如果您的std::deque 实现能够从单个块中擦除(或者它只需要重新排列该块中的元素),那么我会感到惊讶如果它速度较慢。
    • 我怀疑inserterase 对于std::list 会比std::deque 快得多,但这也可以通过deque 的更有效分配来补偿。我也试试看。
    【解决方案3】:

    列表和向量在管理对象的方式上完全不同。

    Vector 将元素就地构造到给定容量的已分配缓冲区中。当容量耗尽时会发生新的分配。 List 一个一个地分配元素,每个元素分配到一个单独分配的空间中。

    当插入/移除某些东西时,向量元素会移动,因此,向量索引和元素地址是不稳定的。 当插入/删除某些内容时,列表元素会重新链接,因此列表迭代器和元素地址是稳定的。

    使列表的行为类似于向量的一种方法是将默认分配器(每次调用时从系统分配)替换为另一个分配器,以更大的块分配对象,将子块分配给列表当它调用它时。 这不是标准库默认提供的。

    【讨论】:

    • @EitanT:“这不是标准库默认提供的东西”的哪一部分。你不明白吗?
    • @NicolBolas 我不是在谈论标准库本身,也许还有其他用于此类目的的“标准”实现。当然,我不是第一个想要为 std::list 预分配内存的人...
    • @NicolBolas 几年前我写过这个:[codeproject.com/Articles/13265/…: 可能需要一些调整以适应 C++11 风格,但它应该可以工作!
    • @EitanT:“你的意思是说……”……哎呀,刚刚修好了!对不起
    【解决方案4】:

    我的建议与 111111 的建议相同,请在编写任何重要代码之前尝试切换到 deque

    但是,要直接回答您的问题:您可以将 std::list 与自定义分配器一起使用。这有点繁琐,我不会在这里详细介绍所有细节,但要点是您编写一个表示列表节点的内存分配策略的类。 list 分配的节点将是一个比char* 大的实现定义的小数量,但它们的大小都相同,这意味着您可以为该大小编写一个优化的分配器(内存块池而不是对象池),您可以向其中添加函数,让您在需要的时候在分配器中保留您想要的任何空间。然后列表可以快速分配/释放。这使您无需重新实现任何实际的 list 功能。

    如果您(出于某种原因)要实现一个具有列表功能的对象池,那么您可以从boost::intrusive 开始。这在编写您自己的分配器时也可能很有用,用于跟踪您的空闲块列表。

    【讨论】:

    • +1:感谢您的建议。我想节省时间,所以在决定自己写任何复杂的东西之前,我想听听其他建议。我按照 111111 的建议,切换到deque。到目前为止它对我有用(主要是因为我的大部分插入都是push_backs 而不是在中间)。
    【解决方案5】:

    您可以使用std::list 中的splice 函数来实现一个池。添加一个新的成员变量PtrList m_Pool。当要添加新对象且池不为空时,将值分配给池中的第一个元素,然后将其拼接到列表中。要擦除一个元素,请将其从列表中拼接到池中。

    但如果您不关心元素的顺序,那么deque 会快得多。如果要擦除中间的元素,请将最后一个元素复制到要删除的元素上,然后擦除最后一个元素。

    【讨论】:

    • "但是如果你不关心元素的顺序,那么双端队列会快得多。" - std::deque 也已订购。
    猜你喜欢
    • 2013-06-18
    • 2015-01-04
    • 1970-01-01
    • 2020-09-22
    • 1970-01-01
    • 2010-12-17
    • 1970-01-01
    相关资源
    最近更新 更多