关于矢量记忆分配的聪明才智

时间:2011-07-24 02:19:36

标签: c++ vector performance memory-management push-back

假设我必须迭代一个可能非常大的数字向量,并将偶数和奇数元素复制到新的单独向量中。 (源向量可以有任何比例的均值;它可以是所有均衡,所有可能性,或介于两者之间。)

为简单起见,push_back通常用于此类事情:

for (std::size_t Index; Index < Source.size(); Index++)
{
    if (Source[Index] % 2) Odds.push_back(Source[Index]);
    else Evens.push_back(Source[Index]);
}

然而,我担心如果它被用作类似排序算法的实现的一部分,这将是低效的并且是有害的,其中性能是最重要的。例如,QuickSort涉及分离元素,就像这样。

您可以使用reserve()预先分配内存,因此只需要一次分配,但是您必须迭代整个源向量两次 - 一次计算需要整理的元素数量,再一次用于实际复制。

当然,您可以分配与源向量大小相同的空间量,因为新的向量都不需要保留更多,但这似乎有点浪费。

有没有更好的方法让我失踪? push_back()通常是否可以信任为程序员管理这类事情,还是会对敏感算法造成负担呢?

5 个答案:

答案 0 :(得分:9)

我将回答我认为你真正要问的问题,即“在重算法的内循环中应该避免push_back()吗?”而不是其他人似乎已经阅读了你的帖子,这是“如果我在对大型向量进行无关排序之前调用push_back,这是否重要?”此外,我将从我的经验中回答,而不是花时间追查引文和同行评审的文章。

您的示例基本上是做了两件事,总计CPU成本:它正在读取和操作输入向量中的元素,然后它必须将元素插入到输出向量中。您担心插入元素的成本是因为:

  1. push_back()是一个常量时间(瞬时,真的),当一个向量有足够的空间为一个额外的元素预先保留时,但是当你的预留空间用完时,它会很慢。
  2. 分配内存的成本很高(malloc() is just slow,即使小学生假装new有所不同)
  3. 重新分配is also slow后,将矢量数据从一个区域复制到另一个区域:当push_back()发现它没有足够的空间时,it has to go and allocate a bigger vector, then copy all the elements。 (理论上,对于大小为OS页面的向量,STL的神奇实现可以使用VMM在虚拟地址空间中移动它们而不进行复制 - 实际上I've never seen one that could。)
  4. 过度分配输出向量会导致问题:它会导致碎片化,使得将来的分配更慢;它会烧毁数据缓存,使一切变慢;如果持续存在,它会占用稀缺的可用内存,导致PC上的磁盘分页和嵌入式平台上的崩溃。
  5. 分配输出向量导致问题,因为重新分配向量是O(n)操作,因此重新分配 m 次是O(m×n)。如果STL的默认分配器使用指数重新分配(每次重新分配时使向量的保留两倍于其先前的大小),那么使得线性算法为O(n + n log m)。
  6. 因此,你的直觉是正确的:总是在可能的情况下为你的向量预留空间,不是因为push_back很慢,而是因为它可以触发 慢的重新分配。另外,如果你看一下shrink_to_fit的实现,你会发现它也会进行副本重新分配,暂时使你的内存成本加倍并造成进一步的碎片化。

    这里的问题是你并不总是确切知道输出向量需要多少空间;通常的反应是使用启发式和自定义分配器。默认情况下,为每个输出向量保留n / 2 + k的输入大小,其中k是某个安全范围。这样你通常通常就有足够的空间用于输出,只要你的输入合理平衡,而push_back可以在极少数情况下重新分配。如果你发现push_back的指数行为浪费了太多的内存(当你真正只需要n + 2时你就会保留2n个元素),你可以给它一个自定义的分配器,以较小的线性块扩展矢量大小 - 当然如果向量真的不平衡并且你最终做了很多调整,那么速度会慢得多。

    如果事先没有走过输入元件,就无法始终保留足够的空间;但如果您知道通常的余额是什么样的,那么您可以使用启发式方法对其进行很好的猜测,以便在多次迭代中获得统计性能提升。

答案 1 :(得分:2)

  

当然,您可以分配与源相同的空间量   向量的大小,因为新的向量都不需要超过   那,但这似乎有些浪费。

然后通过致电shrink_to_fit

跟进
  

但是,我担心这会导致效率低下并且会对事物造成伤害   喜欢排序算法。 ... push_back()通常被信任管理   对于程序员来说,这种事情,或者它会变得很麻烦   敏感算法?

是的,push_back是值得信赖的。老实说,我不明白你的顾虑是什么。据推测,如果你在向量上使用算法,你已经将元素放入向量中。你在谈论什么样的算法如何向量元素到达那里,是push_back还是别的什么?

答案 2 :(得分:2)

如何使用自定义谓词对原始向量进行排序,将所有平均值置于所有赔率之前?

bool EvenBeforeOdd(int a, int b)
{
  if ((a - b)  % 2 == 0) return a < b;

  return a % 2 == 0;
}

std::sort(v.begin(), v.end(), EvenBeforeOdd);

然后你只需要找到最大的偶数,你可以做到,例如使用upper_bound获得非常大的偶数或类似的数字。一旦找到,就可以制作非常便宜的范围副本。

更新:正如@Blastfurnace评论的那样,使用std::partition而不是sort效率更高,因为我们实际上并不需要在每个分区中排序的元素:

bool isEven(int a) { return 0 == a % 2; }
std::vector<int>::const_iterator it =  std::partition(v.begin(), v.end(), isEven);

std::vector<int> evens, odds;
evens.reserve(std::distance(v.begin(), it);
odds.reserve(std::distance(it, v.end());

std::copy(v.begin(), it, std::back_inserter(evens));
std::copy(it, v.end(), std::back_inserter(odds));

答案 3 :(得分:1)

如果您的对象是动态创建的,那么向量实际上只是存储指针。这使得向量更加高效,特别是在内部重新分配时。如果多个位置存在相同的对象,这也可以节省内存。

std::vector<YourObject*> Evens;

注意:不要从函数上下文中推送指针,因为这会导致该帧之外的数据损坏。相反,对象需要动态分配。

这可能无法解决您的问题,但可能会有用。

答案 4 :(得分:1)

如果您的子向量正好是一半(奇数/偶数),则只需为每个向量分配50%的原始向量。这样可以避免浪费和shrink_to_fit