【问题标题】:Heap allocation vs std::vector堆分配与 std::vector
【发布时间】:2020-08-04 07:17:25
【问题描述】:

我需要在我的代码的关键部分创建一个多树。标准方法是使用类似的东西:

struct node{ // or class
   ... data ...
  std::unordered_set<node*> children
};

node* root;

重要的一点是,一旦创建了一个节点,在整个树被删除之前,on 不会被删除。

显而易见的方法是使用 newdelete 语句来管理堆上的这些数据。完成。

我想知道是否值得探索直接使用 std::vector 而不是 new/delete 的性能:

struct node{ // or class
   ... data ...
  std::unordered_set<uint_32> children
};

std::vector<node> tree;
uint_32 root; // probably always 0, fist element of vector

删除树只需使用tree.clear();

对于大(或巨大的)树有没有可能更快?

【问题讨论】:

  • 为什么使用原始指针? std::unordered_set&lt;str::unique_ptr&lt;node&gt;&gt; children 和删除树就像children.clear() 一样简单
  • 一个向量需要随着它的增长而重新分配。但与分配每个节点相比,它需要的分配更少。如果您可以粗略预测节点的数量,您可以减少重新分配的需求,所以是的,可能值得对其进行测量。
  • 你为什么使用sdt::unordered_set?这是一个相当沉重和缓慢的数据结构。在最坏的情况下,每个节点可以有多少个孩子?这个数字有界吗?
  • 规范本身并没有讨论 heapstack,它们只是创建满足要求的 c++ 实现的一种方式眼镜。在常见的实现中,默认的new(如果没有重载或新的)和std::vector都会在堆中创建元素。
  • 我认识的一位研究人员为某些模型构建巨大的树担心删除时间(=读取树以删除每个节点),他最终在不同的子进程中创建整个数据并在他完成了,操作系统正在快速清除它。不过,关于分配,它对您的问题没有帮助,但我相信您对大分配的想法可能会更有效。

标签: c++ performance containers heap-memory c++-standard-library


【解决方案1】:

我猜你有一棵大小超过 1000 万个元素(或至少在 100k+ 个元素的范围内)的树来处理这个问题。

这一切都取决于您的数据和树的使用方式。数据的空间性通常是值得关注的,但如果这也是你的情况,它并不明显。如果树在构建后被遍历很多次(与被修改很多次相反)并且只被修改了一点,那么尝试组织数据以使其迭代连续是有意义的(就像std::vector 通常是) .在这种情况下,您确实想要这样的指针,或者您确实想要使用单个元素new/delete,因为这几乎是保证会让你失望(因为你不能指望元素在内存中是连续放置的)——但这完全取决于你的用例。

一个简单的方法和根本不同的方法是:

std::vector< std::pair<PathInTree, Node> > tree;

然后对其进行排序,使Nodes 按照您迭代树的顺序出现。这是一个有点极端的解决方案,并且有一些缺点,包括如果进行大量随机插入,现在插入会变得很昂贵。我也省略了如何在树中实现路径,但它通常应该是某种序列(如在文件系统结构中)。

【讨论】:

  • 确实上下文是随机数据,大小至少百万。不要认为对节点进行排序是一种选择。树建好后,数据只处理一次,树就被销毁了。使用 Node 而不是 Node¨* 或 unique_ptr 是一种选择。我从来没有做过,只是尝试过。在某些时候,我需要写类似for(auto&amp; node: nodes) {...} 的东西。这里的问题是 node 然后是 const 而我需要在循环中更改它的内容。
【解决方案2】:

考虑到std::unordered_set&lt;uint_32&gt; 的每个实例仍将导致在堆上(内部)至少再分配一次,实际可实现的节省有一个硬性限制。您将节省不到 50% 的必要堆分配。

一个重要的事情是,一旦一个节点被创建,它不会被删除,直到整个树被删除。

随着 std::vector 的增长,重新分配仍在发生。与堆上的不可移动分配相比,这还涉及移动已分配的节点。根据您的 node 元素具有的其他属性,这可能会更加昂贵。您还需要确保您的 node 类与移动语义兼容。

std::unordered_set 也在堆上分配,您也无法避免由此产生的内存碎片。

总而言之,除非您希望受到堆分配性能的限制,或者您有一个与顺序无关的树遍历的用例,否则压缩node 实例可能不值得。


一个可能合理的优化是将std::unordered_set&lt;node*&gt; 替换为std::unordered_set&lt;node&gt;。除非您在那个地方需要这种间接性(外部指针?),否则这会像您自己的尝试一样为您节省很多,而不会牺牲调试功能。

node 实现必要的哈希函数和相等运算符通常很容易。


如果性能是一个真正的问题,并且您对每个节点的子节点数量也有一个(合理的)上限,您可以考虑将std::unordered_set&lt;uint_32&gt; children 替换为std::array&lt;uint_32, n&gt; children,而不是使用std::find() 来检测冲突。

这样做的原因很简单,确保node 变为TriviallyCopyable,这减少了内部对普通memcpy 的重新分配。对于多达 16 个左右的孩子,这在内存消耗方面可能是中性的,但仍然更快。

【讨论】:

  • 我的主题不是堆栈与堆分配。它与在堆中生成大量小块与分配一个大块(具有重新分配、内部容器数据管理等)有关。
  • @Jacques 您仍然无法节省超过 50% 的分配。如果你不能同时消除不可避免的小块造成的碎片,那么分配一个大块也不会像你希望的那样节省。
猜你喜欢
  • 2022-01-24
  • 1970-01-01
  • 1970-01-01
  • 2014-12-12
  • 1970-01-01
  • 1970-01-01
  • 2020-09-22
相关资源
最近更新 更多