为什么树结构要比其节点总和占用更多空间

时间:2019-09-05 14:12:56

标签: c++

填充以下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";
}

1 个答案:

答案 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