填充以下n元树数据结构将创建64'570'080个节点,这将占用〜1480mb的存储空间(对于x64构建,每个节点24字节)。但是该程序的实际内存占用量约为1900mb(如Visual Studio和任务管理器所指示)。当我不填充树,而是将相同数量的节点推入向量时,占用空间约为预期的1480mb。
为什么树要比向量中相同数量的节点占用更多的空间,我该如何解决?我使用最新的MSVC编译器。
struct Node
{
public:
void AddChild()
{
if (first_child_ == nullptr)
{
first_child_ = std::make_unique<Node>();
first_child_->parent_ = this;
}
else
{
Node* next = first_child_.get();
while (next->next_sibling_ != nullptr)
{
next = next->next_sibling_.get();
}
next->next_sibling_ = std::make_unique<Node>();
next->next_sibling_->parent_ = this;
}
}
class NodeRange;
NodeRange GetChildren();
Node* GetNextSibling() { return next_sibling_.get(); }
private:
// Pointer to the parent node. nullptr for the root.
Node* parent_ = nullptr;
// Pointer to the first child. nullptr for a leaf node.
std::unique_ptr<Node> first_child_;
// Pointer to the next sibling. nullptr if there are no further siblings.
std::unique_ptr<Node> next_sibling_;
};
class NodeIterator
{
public:
NodeIterator(Node* node) : node_(node) {}
Node* operator*() { return node_; }
Node* operator->() { return node_; }
bool operator==(NodeIterator& other) { return node_ == other.node_; }
bool operator!=(NodeIterator& other) { return node_ != other.node_; }
void operator++() { node_ = node_->GetNextSibling(); }
private:
Node* node_;
};
class Node::NodeRange
{
public:
NodeIterator begin() { return NodeIterator(node_); }
NodeIterator end() { return NodeIterator(nullptr); }
private:
NodeRange(Node* node) : node_(node) {}
Node* node_;
friend class Node;
};
Node::NodeRange Node::GetChildren() { return first_child_.get(); }
#define MAX_DEPTH 16
#define BRANCHING_FACTOR 3
std::unique_ptr<Node> tree;
size_t nodeCount = 0;
void Populate(Node& node, int currentDepth = 0)
{
if (currentDepth == MAX_DEPTH) return;
for (size_t i = 0; i < BRANCHING_FACTOR; i++)
{
node.AddChild();
nodeCount++;
}
for (Node* child : node.GetChildren())
{
Populate(*child, currentDepth + 1);
}
}
int main()
{
tree = std::make_unique<Node>();
Populate(*tree.get());
std::cout << "Nodes: " << nodeCount << "\n";
std::cout << "Node size: " << sizeof(Node) << "\n";
std::cout << "Estimated tree size, bytes: " << (nodeCount * sizeof(Node)) << "\n";
std::cout << "Estimated tree size, mb: " << (nodeCount * sizeof(Node) / 1024.0 / 1024.0) << "\n";
}
答案 0 :(得分:6)
由于每个树节点是分别分配的,因此每个堆内存分配都有开销,因此堆分配器将其内部维护信息与每个分配的块一起存储。在GNU malloc的开销为8个字节的64位系统上,MSVC运行时库可能具有不同的非零开销(但看起来也为8个字节)。有关更多详细信息,请参见MallocInternals。
使开销最小化的一种方法是从大型的预分配数组中分配树节点。一个示例是boost::pool。
使用std::unique_ptr
存储子节点可能会由于递归调用而导致堆栈溢出:~Node()
调用first_child_->~std::unique_ptr<Node>()
并调用~Node()
,后者又调用first_child_->~std::unique_ptr<Node>()
,依此类推上,这可能会使堆栈溢出。
一种解决方案是将first_child_
和next_sibling_
用作普通的Node*
指针,并实现类Tree
和代码~Tree()
中的代码,该代码无需递归即可遍历树并手动销毁树节点。在这种情况下,Tree
拥有其Node
。