【问题标题】:std::vector< std::vector<unsigned char> > or std::deque< std::vector<unsigned char> >?std::vector< std::vector<unsigned char> > 还是 std::deque< std::vector<unsigned char> >?
【发布时间】:2012-03-07 04:12:46
【问题描述】:

我有一个现有的算法,如果可能的话,我需要对其进行优化。目前无法在此算法中进行大量更改。该算法适用于std::vector&lt; std::vector&lt;unsigned char&gt; &gt; 的实例。它看起来像这样:

typedef std::vector<unsigned char> internal_vector_t;
std::vector< internal_vector_t > internal_vectors; 

while (fetching lots of records) {
   internal_vector_t tmp;
   // reads 1Mb of chars in tmp...
   internal_vectors.push_back(tmp);
   // some more work
}

// use this internal_vectors

该算法使用 push_back() 在 internal_vectors 的 internal_vector_t 实例中插入很多次。 大多数 internal_vector_t 实例的大小为 1 Mb。由于internal_vectors 的大小未知,因此没有预先完成reserve()。

我不明白的第一件事是当internal_vectors 达到其当前容量时会发生什么,需要分配一个新块并将其当前内容复制到更大的内存块中。由于大多数块的大小为 1Mb,因此复制是一个很长的操作。 我是否应该期望编译器(gcc 4.3、MS VC++ 2008)能够对其进行优化以避免复制

如果无法避免复制,是否将更改为std::deque 帮助?我考虑 std::deque 因为我仍然需要通过像 internal_vectors[10] 这样的索引来访问。像这样:

typedef std::vector<unsigned char> internal_vector_t;
std::deque< internal_vector_t > internal_vectors; 
// the same while

据我了解,std::deque 不需要重新分配曾经分配的位置。我对 std::deque 在这种情况下会要求在 push_backs 上进行更少的分配和复制是对的吗?


更新:
1) 根据DeadMG MSVC9 进行此类优化(Swaptimization - TR1 Fixes In VC9 SP1)。 gcc 4.3 可能不做这种优化。

2) 我已经分析了使用std::deque&lt; std::vector&lt;unsigned char&gt; &gt; 的算法版本,我发现它的性能更好。

3) 我还使用了Mark Ransom 建议的swap。使用它可以提高性能:

   internal_vector_t tmp;
   internal_vectors.push_back(empty);
   tmp.swap(internal_vectors.back());

【问题讨论】:

  • 您使用的是insert 还是push_back?代码上写着insert,文本上写着push_back,对于一个向量来说,两者的成本是完全不同的。
  • 当容量用完时,显然它必须分配更多的 RAM。它基于增量值执行此操作。增量和初始容量都应该是可设置的。增量值越高,每次用完时分配的内存就越多。
  • 只是reserve() 一大块(2048 年?),应该可以解决问题...
  • 我使用push_back,在我的问题中修复了它
  • @skwllsp:那么您的代码示例与您的问题无关,我们无法进一步提供可靠的帮助。

标签: c++ optimization vector memory-management deque


【解决方案1】:

MSVC9 为其标准容器实现了一种称为“swaptimization”的东西。这是移动语义的较弱版本。当外部向量被调整大小时,它不会复制内部向量。

但是,您最好将编译器升级到 MSVC10 或 GCC(我认为是 4.5),这将为您提供移动语义,从而大大提高此类操作的效率。当然,std::deque 可能仍然是更智能的容器,但移动语义在很多很多地方都对性能有益。

【讨论】:

  • gcc中有没有类似swaptimization的东西?你提到了 gcc 4.5。
  • @skwllsp:正确的版本是 Move Semantics,它是 C++11 的特性。您可以在 MSVC10 和一些最新版本的 GCC、4.4 或 4.5 中找到它
【解决方案2】:

每次将internal_vector_t 插入internal_vectors 时,它都会复制internal_vector_t。无论您使用vector 还是deque,这都是正确的。标准容器始终会复制您正在插入的对象。

您可以通过插入一个空的internal_vector_tswap 将插入对象的内容与您真正想要插入的对象相匹配来消除复制。

在插入过程中向量空间不足时,有时需要调整自身大小,这会导致对象再次被复制。只要您始终在开头或结尾插入,双端队列就会消除这种情况。

编辑:我上面给出的建议可以用这些代码更改来概括。这段代码应该避免所有大向量的复制。

typedef std::vector<unsigned char> internal_vector_t;
std::deque< internal_vector_t > internal_vectors; 
internal_vector_t empty;

while (fetching lots of records) {
   internal_vector_t tmp;
   // reads 1Mb of chars in tmp...
   internal_vectors.push_back(empty);
   tmp.swap(internal_vectors.back());
   // some more work
}

【讨论】:

  • 其实我主要对优化这个感兴趣:Occasionally the vector will need to resize itself as it runs out of room during an insertion,因为这是我分析代码时第二个最常调用的函数。
  • @skwllsp, vector 通常设计为分配更大的数量,以便随着要复制的项目数量的增加,复制频率降低。你能知道电话是来自internal_vector_t还是internal_vectors
  • 是的,我可以。电话来自std::vector&lt; std::vector &lt;uchar&gt; &gt;::M_fill_insertstd::vector&lt;uchar&gt;::M_fill_insertstd::vector&lt; std::vector &lt;uchar&gt; &gt;::M_fill_insert 需要更多时间来处理。这就是为什么我询问可能的优化和使用 std::deque
  • @skwllsp,由于您使用的是push_back,因此您不会使用 std::deque 产生任何复制开销,因此在您的情况下它似乎是一个不错的选择。您为什么不直接尝试一下,看看它是否有所改进?
  • @DeadMG,很有趣。我曾考虑过这样的优化是可能的,但是编译器如何知道这些类型是可交换的?如果类型没有专门化std::swap,那不会导致悲观吗?
【解决方案3】:

std::deque 不会连续存储它的元素 - 它将它的存储分成一系列恒定大小的“块”。这意味着当std::deque 容量不足时,它只需要分配一个恒定大小的新块 - 它不需要重新分配整个内部缓冲区并移动所有现有元素。

另一方面,std::vector 确实保持连续存储,因此当它用完容量并重新分配时,它确实需要移动所有现有元素 - 这可能很昂贵。

std::vector 对其重新分配方案“聪明”,根据几何级数分块分配(通常将容量加倍或增加 1.5 等)。这意味着重新分配不会经常发生。

std::deque 在这种情况下可能更有效,因为当重新分配确实发生时,它所做的工作更少。与往常一样,您必须进行基准测试才能获得任何实数。

您的代码可能会在其他方面得到进一步改进。似乎在while 循环的每次迭代中,您都在创建一个新的internal_vector_t tmp。在循环之外声明它可能更有效,并且在每次迭代时只需 ::clear() 它的存储。每次调用 internal_vectors.push_back(tmp) 时,您也会复制整个 tmp 向量 - 您可以通过 internal_vectors.push_back(std::move(tmp)) 移动 tmp 向量来改进这一点 - 这只会复制一些指针。

希望这会有所帮助。

【讨论】:

  • 我基本上只将 std::deque 用于 FIFO 队列,或者如果我需要一个通常占用我一半以上 RAM 的不断增长的容器(非常罕见)。
  • @MooingDuck:我认为还有其他用例。如果您不知道::reserve 有多少空间并且尺寸最终可能会很大(基本上是这个问题),我会看看std::deque。不仅std::vector 的重新分配成本可能是一个问题,而且重复重新分配时内存碎片的可能性也是一个问题。通常我发现std::deque 在大小
  • 诀窍是deque 倾向于比向量进行更多 分配,尽管它没有副本,这使得比较变得困难。对于推回 5000 int,MSFT 的 vector 将进行 ~19 次分配,deque ~1250 次分配。对于 gcc,分别为 ~12 和 ~39。但是双端队列不会复制。 @skwllsp:简介!
  • @MooingDuck:众所周知,(当前)MSVC std::deque 存在严重缺陷,因为它们以疯狂的16 bytes 小块分配,导致您提到的行为。出于这个原因,我不使用 MSVC 容器,我认为基于一个特定的 std 库的实现问题进行概括是不公平的。
  • deque 很有用,但通常向量是正确的答案。至于基于一种实现的概括,这就是为什么我列出了两个最常见的实现。
【解决方案4】:

您是否正在索引外部向量?如果没有,std::list&lt;std::vector&lt;unsigned char&gt; &gt; 怎么样?

【讨论】:

  • 是的,我在我的问题中提到了它。我考虑使用 std::deque 因为我仍然需要像 internal_vectors[10] 这样的索引访问。
  • @skwllsp 你真的需要随机访问吗?您可能会通过遍历列表而侥幸逃脱。只需增加一个计数器并检查您需要的元素之一是否在该索引处。我希望我说得通
  • @pezcode Do you actually need random access? 不确定。但遗憾的是,这种算法的重大变化不是一种选择。
【解决方案5】:

出队可能更有效,具体取决于实现。与向量不同,出队不能保证连续存储,因此可以分配几个单独的内存块。因此,它可以分配更多内存而无需移动已添加的元素。您应该尝试一下并衡量其影响。

【讨论】:

  • +1 用于分析,因为我认为这种情况下的差异将在纳秒内。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2014-12-11
  • 1970-01-01
  • 2012-05-13
  • 2013-11-18
  • 1970-01-01
  • 2018-12-02
  • 2021-07-11
相关资源
最近更新 更多