【问题标题】:Subdivide Quadtree without the new operator在没有新运算符的情况下细分四叉树
【发布时间】:2019-01-23 10:27:00
【问题描述】:

在我见过的每个四叉树实现中,细分方法总是使用new 运算符来创建子单元格。

有没有办法避免这种情况? 因为我每帧都重新创建四叉树以轻松更新它,但是每帧使用 newdelete 大约 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;
}

在使用 clear() 函数删除整棵树之后,我会为每帧添加到四叉树的每个点(大约 1000 个)执行此操作。

void UQuadtree::clear() {

    if (root->NW != nullptr) {
        delete root->NW;
        root->NW = nullptr;
        delete root->NE;
        root->NE = nullptr;
        delete root->SW;
        root->SW = nullptr;
        delete root->SE;
        root->SE = nullptr;
    }
}

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

【问题讨论】:

  • 请给我们具体的实现。否则你不会得到任何具体的答案。不过,您可以考虑重载new 以创建内存池。
  • "newdelete 每帧大约 200 ~ 300 次会影响我的性能",你真的进行了基准测试吗?
  • 你没有证据表明new 是这里的罪魁祸首,作为一个可行的假设,它是相当可疑的。您需要分析您的代码并确定实际的瓶颈。无论如何,不​​要在每一帧都重新创建整个树可能是个好主意。
  • 为了防止单独分配,您可以提供尚未使用的预分配节点块。当节点被细分时,它们可以从该池中取出。 (如果细分反转,它们可以被归还 - 这就是我对@wilx 的理解。)如果预先分配的节点被完全消耗并且需要更多,那么必须分配一个新的块。这至少会减少单独分配的数量,甚至可能通过更好的缓存局部性来提高性能......
  • 在最简单的情况下,内存池可能是节点向量列表 + 基于该池管理未使用和已使用节点的一些操作。请注意,虽然向量通过给它一个初始大小来提供方便的分配 - 调整它的大小可能会使它的所有内容无效。 (因此,我提到了一个向量列表。)如果您想归还单个节点,“管理未使用节点与已使用节点的 + 有点摆弄”可能会变得更加复杂。否则,某种计数器可能足以识别向量何时用尽(并且需要一个新的)。

标签: c++ memory-management quadtree


【解决方案1】:

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

首先,我做了一些约束来简化概念:

  1. 节点提供默认构造函数。
  2. 无需就地构建节点。
  3. 节点是连续创建并一次性释放的。

所以,我从template class 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; }
};

块被实现为std::vector&lt;ELEMENT&gt;,块列表只是一个std::list&lt;std::vector&lt;ELEMENT&gt;&gt;

ELEMENT&amp; getNew() 是从池中请求新元素的函数。

如果当前块耗尽,则执行切换到下一个块。

如果它是最后一个块,则分配一个新块并将其添加到列表中。

之后,从chunk中返回下一个元素。

请注意,我禁用了PoolT 的复制构造函数和复制赋值运算符。我认为复制内存池没有任何意义。因此,如果它是意外完成的(例如,当打算通过引用将其作为参数传递给函数时忘记插入 &amp;),这将导致编译器错误。

对于元素,我制作了一个struct Node,它类似于 OPs 四叉树节点的一部分:

struct Node {
  Node *pNW, *pNE, *pSW, *pSE;

  Node(): pNW(nullptr), pNE(nullptr), pSW(nullptr), pSE(nullptr) { }
  ~Node() = default;

  Node(const Node&) = delete;
  Node& operator=(const Node&) = delete;

  void clear()
  {
    pNW = pNE = pSW = pSE = nullptr;
  }
};

返回的ELEMENT 可能以前被使用过。因此,它应该在之后重置为初始状态。为了简单起见,我只是创建了一个函数Node::clear(),它将实例重置为初始状态。

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

Node 的内存池:

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;
}

编译和测试:

NodePool nodePool;
  PoolT<ELEMENT>::PoolT()
nodePool.capacity(): 0, nodePool.size(): 0
Node *pRoot = nullptr;
pRoot = fill(nodePool, 2);
  PoolT<ELEMENT>::getNew(): Chunks exhausted. Allocating new chunk of size 16.
  PoolT<ELEMENT>::getNew(): returning ELEMENT 0 of current chunk.
  PoolT<ELEMENT>::getNew(): returning ELEMENT 1 of current chunk.
  PoolT<ELEMENT>::getNew(): returning ELEMENT 2 of current chunk.
  PoolT<ELEMENT>::getNew(): returning ELEMENT 3 of current chunk.
  PoolT<ELEMENT>::getNew(): returning ELEMENT 4 of current chunk.
std::cout << "pRoot: "; print(std::cout, pRoot);
pRoot: 0xcb4c30
  pNW: 0xcb4c50
  pNE: 0xcb4c70
  pSW: 0xcb4c90
  pSE: 0xcb4cb0
nodePool.capacity(): 16, nodePool.size(): 5
pRoot = nullptr;
nodePool.reset();
nodePool.capacity(): 16, nodePool.size(): 0
pRoot = fill(nodePool, 3);
  PoolT<ELEMENT>::getNew(): returning ELEMENT 0 of current chunk.
  PoolT<ELEMENT>::getNew(): returning ELEMENT 1 of current chunk.
  PoolT<ELEMENT>::getNew(): returning ELEMENT 2 of current chunk.
  PoolT<ELEMENT>::getNew(): returning ELEMENT 3 of current chunk.
  PoolT<ELEMENT>::getNew(): returning ELEMENT 4 of current chunk.
  PoolT<ELEMENT>::getNew(): returning ELEMENT 5 of current chunk.
  PoolT<ELEMENT>::getNew(): returning ELEMENT 6 of current chunk.
  PoolT<ELEMENT>::getNew(): returning ELEMENT 7 of current chunk.
  PoolT<ELEMENT>::getNew(): returning ELEMENT 8 of current chunk.
  PoolT<ELEMENT>::getNew(): returning ELEMENT 9 of current chunk.
  PoolT<ELEMENT>::getNew(): returning ELEMENT 10 of current chunk.
  PoolT<ELEMENT>::getNew(): returning ELEMENT 11 of current chunk.
  PoolT<ELEMENT>::getNew(): returning ELEMENT 12 of current chunk.
  PoolT<ELEMENT>::getNew(): returning ELEMENT 13 of current chunk.
  PoolT<ELEMENT>::getNew(): returning ELEMENT 14 of current chunk.
  PoolT<ELEMENT>::getNew(): returning ELEMENT 15 of current chunk.
  PoolT<ELEMENT>::getNew(): switching to next chunk.
  PoolT<ELEMENT>::getNew(): Chunks exhausted. Allocating new chunk of size 16.
  PoolT<ELEMENT>::getNew(): returning ELEMENT 0 of current chunk.
  PoolT<ELEMENT>::getNew(): returning ELEMENT 1 of current chunk.
  PoolT<ELEMENT>::getNew(): returning ELEMENT 2 of current chunk.
  PoolT<ELEMENT>::getNew(): returning ELEMENT 3 of current chunk.
  PoolT<ELEMENT>::getNew(): returning ELEMENT 4 of current chunk.
std::cout << "pRoot: "; print(std::cout, pRoot);
pRoot: 0xcb4c30
  pNW: 0xcb4c50
    pNW: 0xcb4c70
    pNE: 0xcb4c90
    pSW: 0xcb4cb0
    pSE: 0xcb4cd0
  pNE: 0xcb4cf0
    pNW: 0xcb4d10
    pNE: 0xcb4d30
    pSW: 0xcb4d50
    pSE: 0xcb4d70
  pSW: 0xcb4d90
    pNW: 0xcb4db0
    pNE: 0xcb4dd0
    pSW: 0xcb4df0
    pSE: 0xcb4e10
  pSE: 0xcb4e70
    pNW: 0xcb4e90
    pNE: 0xcb4eb0
    pSW: 0xcb4ed0
    pSE: 0xcb4ef0
nodePool.capacity(): 32, nodePool.size(): 21

Live Demo on coliru

我使用了一个相当小的N = 16 作为默认块大小。我这样做是为了显示块的疲惫,而无需将样本大小吹得太大。对于“生产性”使用,我当然会推荐更高的值。

当然,有很多潜力可以让这个更复杂,获得 C++ 功能,如重载 newdelete、就地构造(如在 std::vector::emplace() 中)或其他令人兴奋的事情。

【讨论】:

  • 这确实令人兴奋!非常感谢您花时间制作这个演示,我认为它会成为一个有用的资源。我对 C++ 很陌生,我使用它才 2 个月,所以我需要一段时间才能理解新概念。但这太清楚了。我需要重新通读一遍并尝试自己完成它,这样我才能吸收它。我今天正在阅读有关重载new 的内容,使用起来似乎很酷。我知道就地构造是什么,但我稍后会读到它。如果你有其他类似的很酷的东西可以引导我,我会很高兴阅读它!
  • @MaximeParata 欢迎您。我插入了一些关于已删除的复制构造函数等的附加注释,并找到了就地构造的链接。
  • 我尝试自己实现池,但是我没有得到GetNew()函数的返回语句。 (*_ItrEnd)[++size, n++]; 到底是什么意思?通常运算符 [x, y] 用于二维数组,对吗?但是在这里,即使我们有一种二维数组,运算符 [] 也不应该与列表一起使用。我知道它实际上是返回 *_ItrEnd[n++] 的内容,但我不清楚整个声明。你能告诉我它应该如何工作吗?
  • @MaximeParata 否。C++ 中没有二维下标运算符。 (*_iterEnd) 引用列表中的当前元素。那是vector[_n++] 检索它的 _nth 元素,而 _n 之后递增。结果是Element&amp;,这就是返回的内容。 ++_size 是一个预期的副作用,我使用序列运算符 , 将其放入下标表达式中。 ++_size 的结果被丢弃,但效果(_size 的增量)在返回 [_n] 的结果之前立即发生,这就是我想要的。但是,++_size; return (*_iterEnd)[_n++]; 也可以。
  • 好的,我现在明白了。它非常简洁和简化,我什至不知道这是可能的。谢谢。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2014-06-14
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多