C++ 不使用新运算符细分四叉树

C++ 不使用新运算符细分四叉树,c++,memory-management,quadtree,C++,Memory Management,Quadtree,在我看到的每个四叉树实现中,细分方法总是使用new操作符来创建子单元 有没有办法避免这种情况? 因为我每帧都重新创建四叉树以方便更新,但是使用new和delete大约每帧200~300次会破坏我的性能 这是我的实现: void UQuadtree::subdivide(Quad * Node) { float HalfExtent = Node->Extent/2; FVector2D Center = Node->Center; Node->NW =

在我看到的每个四叉树实现中,细分方法总是使用
new
操作符来创建子单元

有没有办法避免这种情况? 因为我每帧都重新创建四叉树以方便更新,但是使用
new
delete
大约每帧200~300次会破坏我的性能

这是我的实现:

void UQuadtree::subdivide(Quad * Node)
{
    float HalfExtent = Node->Extent/2;
    FVector2D Center = Node->Center;

    Node->NW = new Quad(FVector2D(Center.X + HalfExtent, Center.Y - HalfExtent), HalfExtent);
    Node->NE = new Quad(FVector2D(Center.X + HalfExtent, Center.Y + HalfExtent), HalfExtent);
    Node->SW = new Quad(FVector2D(Center.X - HalfExtent, Center.Y - HalfExtent), HalfExtent);
    Node->SE = new Quad(FVector2D(Center.X - HalfExtent, Center.Y + HalfExtent), HalfExtent);
}

bool UQuadtree::insert(FVector2D* point, Quad * Node)
{
    if (!ConstructBox2D(Node->Center, Node->Extent).IsInside(*point)) 
    {
        return false;
    }
    if (Node->Points.Num() < Capacity) {
        Node->Points.Add(point);

        return true;
    }
    if (Node->NW == nullptr) {
        subdivide(Node);
    }
    if (insert(point, Node->NW)) { return true; }
    if (insert(point, Node->NE)) { return true; }
    if (insert(point, Node->SW)) { return true; }
    if (insert(point, Node->SE)) { return true; }

    return false;
}

(顺便说一句,我是在UE4中实现的)。

我想演示一个非常简单的内存池。(在我的评论中,我推荐了一个向量列表,这就是我想在下面详细阐述的内容。)

首先,我提出了一些用于简化概念的约束条件:

  • 节点提供默认构造函数
  • 不需要节点的就地构造
  • 节点是连续创建的,一次释放所有节点
  • 因此,我从一个
    模板类PoolT
    开始:

    #include <iomanip>
    #include <iostream>
    #include <vector>
    #include <list>
    
    template <typename ELEMENT, size_t N = 16>
    class PoolT {
      private:
        typedef std::list<std::vector<ELEMENT> > Data;
        Data _data;
        typename Data::iterator _iterEnd;
        size_t _n;
        size_t _size, _capacity;
    
      public:
        PoolT():
          _data(), _iterEnd(_data.end()), _n(N),
          _size(0), _capacity(0)
        {
          std::cout << "  PoolT<ELEMENT>::PoolT()\n";
        }
        ~PoolT() = default;
    
        PoolT(const PoolT&) = delete;
        PoolT& operator=(const PoolT&) = delete;
    
        ELEMENT& getNew()
        {
          if (_n >= N && _iterEnd != _data.end()) {
            _n = 0; ++_iterEnd;
            std::cout << "  PoolT<ELEMENT>::getNew(): switching to next chunk.\n";
          }
          if (_iterEnd == _data.end()) {
            std::cout << "  PoolT<ELEMENT>::getNew(): Chunks exhausted. Allocating new chunk of size " << N << ".\n";
            _iterEnd = _data.insert(_iterEnd, std::vector<ELEMENT>(N));
            _capacity += N;
            _n = 0;
          }
          std::cout << "  PoolT<ELEMENT>::getNew(): returning ELEMENT " << _n << " of current chunk.\n";
          return (*_iterEnd)[++_size, _n++];
        }
    
        void reset()
        {
          _size = _n = 0; _iterEnd = _data.begin();
        }
    
        size_t size() const { return _size; }
        size_t capacity() const { return _capacity; }
    };
    
    返回的
    元素
    可能以前使用过。因此,之后应将其重置为初始状态。为了简单起见,我只做了一个函数
    Node::clear()
    ,它将实例重置为初始状态

    我还禁用了
    节点
    的复制构造函数和复制赋值操作符。在我的示例中,
    节点
    实例通过指针相互引用。因此,重新分配它们的存储将产生致命的后果。(这会使节点指针悬空。)内存池
    PoolT
    的构建就是考虑到这一点的。(对于
    std::vector
    中的意外重新分配,至少需要其中一个(复制构造函数或赋值运算符)。因此,在这种情况下,我会得到一个编译器错误。)

    节点的内存池

    typedef PoolT<Node> NodePool;
    
    typedef PoolT NodePool;
    
    和一个小测试套件,用于显示实际情况:

    Node* fill(NodePool &nodePool, int depth)
    {
      Node *pNode = &nodePool.getNew();
      pNode->clear();
      if (--depth > 0) {
        pNode->pNW = fill(nodePool, depth);
        pNode->pNE = fill(nodePool, depth);
        pNode->pSW = fill(nodePool, depth);
        pNode->pSE = fill(nodePool, depth);
      }
      return pNode;
    }
    
    void print(std::ostream &out, const Node *pNode, int depth = 0)
    {
      out << (const void*)pNode << '\n';
      if (!pNode) return;
      ++depth;
      if (pNode->pNW) {
        out << std::setw(2 * depth) << "" << "pNW: "; print(out, pNode->pNW, depth);
      }
      if (pNode->pNE) {
        out << std::setw(2 * depth) << "" << "pNE: "; print(out, pNode->pNE, depth);
      }
      if (pNode->pSW) {
        out << std::setw(2 * depth) << "" << "pSW: "; print(out, pNode->pSW, depth);
      }
      if (pNode->pSE) {
        out << std::setw(2 * depth) << "" << "pSE: "; print(out, pNode->pSE, depth);
      }
    }
    
    #define DEBUG(...) std::cout << #__VA_ARGS__ << ";\n"; __VA_ARGS__
    
    int main()
    {
      DEBUG(NodePool nodePool);
      std::cout
        << "nodePool.capacity(): " << nodePool.capacity() << ", "
        << "nodePool.size(): " << nodePool.size() << '\n';
      DEBUG(Node *pRoot = nullptr);
      DEBUG(pRoot = fill(nodePool, 2));
      DEBUG(std::cout << "pRoot: "; print(std::cout, pRoot));
      std::cout
        << "nodePool.capacity(): " << nodePool.capacity() << ", "
        << "nodePool.size(): " << nodePool.size() << '\n';
      DEBUG(pRoot = nullptr);
      DEBUG(nodePool.reset());
      std::cout
        << "nodePool.capacity(): " << nodePool.capacity() << ", "
        << "nodePool.size(): " << nodePool.size() << '\n';
      DEBUG(pRoot = fill(nodePool, 3));
      DEBUG(std::cout << "pRoot: "; print(std::cout, pRoot));
      std::cout
        << "nodePool.capacity(): " << nodePool.capacity() << ", "
        << "nodePool.size(): " << nodePool.size() << '\n';
      return 0;
    }
    
    Node*填充(NodePool&NodePool,int-depth)
    {
    Node*pNode=&nodepol.getNew();
    pNode->clear();
    如果(--深度>0){
    pNode->pNW=填充(节点池,深度);
    pNode->pNE=填充(节点波尔,深度);
    pNode->pSW=填充(节点波尔,深度);
    pNode->pSE=填充(节点波尔,深度);
    }
    返回pNode;
    }
    无效打印(std::ostream&out,常量节点*pNode,int深度=0)
    {
    
    请给出具体的实现。否则您将无法得到任何具体的答案。不过,您可以考虑重载
    new
    ,以创建内存池。“
    new
    delete
    大约每帧200~300次会扼杀我的性能”,您是否真的对其进行了基准测试?您没有证据表明
    new
    是罪魁祸首,并且作为一个有效的假设,它是相当可疑的。您需要分析您的代码并确定实际的瓶颈。在任何情况下,不在每一帧重新创建整个树可能是一个好主意。为了防止单独的分配,您可以查看尚未使用的预分配节点块。当对节点进行细分时,可以将其从该池中取出。(如果进行反向细分,则可以将其退回-这就是我对@wilx的理解。)如果预先分配的节点被完全消耗,并且需要更多的节点,那么必须分配一个新的块。这至少会减少单个分配的数量,甚至可能通过更好的缓存位置来提高性能……内存池在最简单的情况下可以是一个节点向量列表+稍微调整以管理未使用和已使用的节点请注意,虽然向量通过给它一个初始大小提供了方便的分配,但调整它的大小可能会使它的所有内容无效。(因此,我提到了一个向量列表。)“+有点篡改以管理未使用的节点与已使用的节点”如果要返回单个节点,可能会变得更复杂。否则,某种计数器可能足以识别向量何时耗尽(需要一个新的)这真是令人兴奋!非常感谢你花时间做这个演示,我想它会成为一个有用的资源。我对C++来说是很新的,我用它的时间只有2个月,所以我在理解新概念之前需要花上一段时间。但是这很清楚。我需要再读一遍,然后自己去做,所以我可以。n消化它。我今天读了关于重载的
    新的
    ,使用它似乎很酷。Idk什么是就地构造,但我稍后会读到。如果你有其他类似的酷东西要引导我,我很高兴读到它!@MaximeParata欢迎你。我插入了一些关于删除副本构造的附加说明等。并找到了用于就地构造的链接。我试图自己实现该池,但我没有得到
    GetNew()
    函数的返回语句。
    (*\u ItrEnd)[++size,n++];
    的确切含义是什么?通常运算符[x,y]用于2D数组,对吗?但在这里,即使我们有一种2D数组,运算符[]不应该用列表来工作。我知道它实际上是返回了什么在代码> *yTrime[n+] [/COD]。但是整个语句我不清楚。你能告诉我它应该如何工作吗?@ MaxePARATA号。C++中没有2D下标操作符。<代码>(*yTyrEnter)
    引用列表中的当前元素。这是一个
    向量
    检索它的
    \n
    第个元素,而
    \n
    随后递增。结果是
    元素&
    ,这就是返回的内容。
    ++\u大小
    是一个预期的副作用,我使用序列运算符
    将其放入下标表达式中。
    ++\u大小
    的结果是disc但是效果(大小的增加)发生在返回
    [\n]
    的结果之前,这正是我想要的。但是,
    ++\u size;return(*u iterEnd)[\n++];
    也可以工作。好吧,我现在更了解它了。它非常简洁,我甚至不知道这是可能的。
    typedef PoolT<Node> NodePool;
    
    Node* fill(NodePool &nodePool, int depth)
    {
      Node *pNode = &nodePool.getNew();
      pNode->clear();
      if (--depth > 0) {
        pNode->pNW = fill(nodePool, depth);
        pNode->pNE = fill(nodePool, depth);
        pNode->pSW = fill(nodePool, depth);
        pNode->pSE = fill(nodePool, depth);
      }
      return pNode;
    }
    
    void print(std::ostream &out, const Node *pNode, int depth = 0)
    {
      out << (const void*)pNode << '\n';
      if (!pNode) return;
      ++depth;
      if (pNode->pNW) {
        out << std::setw(2 * depth) << "" << "pNW: "; print(out, pNode->pNW, depth);
      }
      if (pNode->pNE) {
        out << std::setw(2 * depth) << "" << "pNE: "; print(out, pNode->pNE, depth);
      }
      if (pNode->pSW) {
        out << std::setw(2 * depth) << "" << "pSW: "; print(out, pNode->pSW, depth);
      }
      if (pNode->pSE) {
        out << std::setw(2 * depth) << "" << "pSE: "; print(out, pNode->pSE, depth);
      }
    }
    
    #define DEBUG(...) std::cout << #__VA_ARGS__ << ";\n"; __VA_ARGS__
    
    int main()
    {
      DEBUG(NodePool nodePool);
      std::cout
        << "nodePool.capacity(): " << nodePool.capacity() << ", "
        << "nodePool.size(): " << nodePool.size() << '\n';
      DEBUG(Node *pRoot = nullptr);
      DEBUG(pRoot = fill(nodePool, 2));
      DEBUG(std::cout << "pRoot: "; print(std::cout, pRoot));
      std::cout
        << "nodePool.capacity(): " << nodePool.capacity() << ", "
        << "nodePool.size(): " << nodePool.size() << '\n';
      DEBUG(pRoot = nullptr);
      DEBUG(nodePool.reset());
      std::cout
        << "nodePool.capacity(): " << nodePool.capacity() << ", "
        << "nodePool.size(): " << nodePool.size() << '\n';
      DEBUG(pRoot = fill(nodePool, 3));
      DEBUG(std::cout << "pRoot: "; print(std::cout, pRoot));
      std::cout
        << "nodePool.capacity(): " << nodePool.capacity() << ", "
        << "nodePool.size(): " << nodePool.size() << '\n';
      return 0;
    }