C++ 八叉树实现的速度问题
几天来,我一直在努力加速我的力导向图的实现。到目前为止,我已经实现了Barnes-Hut算法,它使用八叉树来减少计算量。我已经对它进行了多次测试,与力相关的计算数量确实大大减少了。下面是没有谷仓小屋(蓝线)和有(红线)的节点数的计算图: 尽管现在应该快很多,但事实是,在速度(时间)方面,升级只占了很少的百分比 我想可能是树创建和树放置中的元素造成了这种情况。由于元素不断移动,我需要重新创建每个循环树,直到达到某个停止条件。但若我要花很多时间创建树,那个么我将失去在力计算上获得的时间。至少我是这么想的。以下是我在主文件循环中添加元素的方式:C++ 八叉树实现的速度问题,c++,algorithm,tree,C++,Algorithm,Tree,几天来,我一直在努力加速我的力导向图的实现。到目前为止,我已经实现了Barnes-Hut算法,它使用八叉树来减少计算量。我已经对它进行了多次测试,与力相关的计算数量确实大大减少了。下面是没有谷仓小屋(蓝线)和有(红线)的节点数的计算图: 尽管现在应该快很多,但事实是,在速度(时间)方面,升级只占了很少的百分比 我想可能是树创建和树放置中的元素造成了这种情况。由于元素不断移动,我需要重新创建每个循环树,直到达到某个停止条件。但若我要花很多时间创建树,那个么我将失去在力计算上获得的时间。至少我是这么
void AddTreeElements(Octree* tree, glm::vec3* boundries, Graph& graph)
{
for(auto& node:graph.NodeVector())
{
node.parent_group = nullptr;
if(node.pos[0] < boundries[1][0] && node.pos[0] > boundries[0][0] &&
node.pos[1] > boundries[4][1] && node.pos[1] < boundries[1][1] &&
node.pos[2] < boundries[0][2] && node.pos[2] > boundries[3][2])
{
tree->AddObject(&node.second);
continue;
}
if(node.pos[0] < boundries[0][0])
{
boundries[0][0] = node.pos[0]-1.0f;
boundries[3][0] = node.pos[0]-1.0f;
boundries[4][0] = node.pos[0]-1.0f;
boundries[7][0] = node.pos[0]-1.0f;
}
else if(node.pos[0] > boundries[1][0])
{
boundries[1][0] = node.pos[0]+1.0f;
boundries[2][0] = node.pos[0]+1.0f;
boundries[5][0] = node.pos[0]+1.0f;
boundries[6][0] = node.pos[0]+1.0f;
}
if(node.pos[1] < boundries[4][1])
{
boundries[4][1] = node.pos[1]-1.0f;
boundries[5][1] = node.pos[1]-1.0f;
boundries[6][1] = node.pos[1]-1.0f;
boundries[7][1] = node.pos[1]-1.0f;
}
else if(node.pos[1] > boundries[0][1])
{
boundries[0][1] = node.pos[1]+1.0f;
boundries[1][1] = node.pos[1]+1.0f;
boundries[2][1] = node.pos[1]+1.0f;
boundries[3][1] = node.pos[1]+1.0f;
}
if(node.pos[2] < boundries[3][2])
{
boundries[2][2] = node.pos[2]-1.0f;
boundries[3][2] = node.pos[2]-1.0f;
boundries[6][2] = node.pos[2]-1.0f;
boundries[7][2] = node.pos[2]-1.0f;
}
else if(node.pos[2] > boundries[0][2])
{
boundries[0][2] = node.pos[2]+1.0f;
boundries[1][2] = node.pos[2]+1.0f;
boundries[4][2] = node.pos[2]+1.0f;
boundries[5][2] = node.pos[2]+1.0f;
}
}
}
虽然我不知道怎样才能加快速度。有人对这里能做什么有什么建议吗
编辑:
我做了一些餐巾纸计算,我想还有一个地方可能会导致速度大幅下降。Boundries检入Update
方法看起来做了很多工作,我计算的结果是,在最坏的情况下,由此增加的复杂性是:
元素数*子元素数*面数*最大级别
在我的例子中,等于元素的个数*240
有人能确认一下我的想法是否合理吗?如果我理解正确,您在每个八叉树节点中存储一个指针向量
std::vector<Element*> objects;
正如我从这段代码中了解到的,对于八叉树构建,父节点从父向量返回元素指针,并开始向后推以将适当的元素传递给子节点
如果是这样的话,我可以立即说这是一个主要的瓶颈,甚至不需要测量,因为我以前处理过这样的八叉树实现,将它们的构建提高了10倍以上,并通过简单地使用一个单链表来减少遍历时的缓存未命中率,在这个特殊情况下,与大量微小的向量(每个节点一个)相比,显著减少了涉及的堆分配/释放,甚至改善了空间位置性。我并不是说这是唯一的瓶颈,但这绝对是一个重要的瓶颈
如果是这样的话,我建议:
struct OctreeElement
{
// Points to next sibling.
OctreeElement* next;
// Points to the element data (point, triangle, whatever).
Element* element;
};
struct OctreeNode
{
OctreeNode* children[8];
glm::vec3 vBoundriesBox[8];
// Points to the first element in this node
// or null if there are none.
OctreeElement* first_element;
float combined_weight;
bool leaf;
};
这实际上只是一个初步的过程,但应该会有很大帮助。然后,当您将一个元素从父元素转移到子元素时,就不会有推回和弹出,也不会有堆分配。你所做的就是操纵指针。要将元素从父元素传输到子元素,请执行以下操作:
// Pop off element from parent.
OctreeElement* elt = parent->first_element;
parent->first_element = elt->next;
// Push it to the nth child.
elt->next = children[n];
children[n]->first_element = elt;
从上面可以看出,使用链接表示法,我们只需操作3个指针即可从一个节点传输到另一个节点——无需堆分配,无需增加大小、检查容量等。此外,还可以将存储元素的开销降低到每个节点一个指针和每个元素一个指针。每个节点一个向量在内存使用方面往往会非常爆炸,因为向量通常会占用32+字节,即使是在默认情况下构建的向量,因为许多实现在存储数据指针、大小和容量的基础上预先分配了一些内存
还有很多改进的余地,但这第一步应该会有很大帮助,如果您使用有效的分配器(例如,自由列表或顺序分配器)分配OctreeElement*,或者将它们存储在稳定的数据结构中,该结构不会使指针失效,但提供一些连续性,比如std::deque
。如果您愿意做更多的工作,请使用std::vector
存储所有元素(整个树的所有元素,而不是每个节点一个向量),并使用索引将元素链接到该向量中,而不是指针。如果对链表使用索引而不是指针,则可以连续存储所有节点,而无需使用内存分配器,只需使用一个大的旧向量来存储所有内容,并将链接的内存需求减半(假设64位指针和32位索引足够,如果可以使用索引的话)
如果您使用32位索引,您可能也不需要所有32位,在这一点上,您可以使用(比如)31位并将leaf
boolean压缩,这将大大增加节点的大小(大约4个字节,带填充,指针的对齐要求假定该布尔字段为64位)进入第一个元素,或者只需将第一个子索引设置为-1
,以指示叶子,如下所示:
struct OctreeElement
{
// Points to the element data (point, triangle, whatever).
int32_t element;
// Points to next sibling.
int32_t next;
};
struct OctreeNode
{
// This can be further reduced down to two
// vectors: a box center and half-size. A
// little bit of arithmetic can still improve
// efficiency of traversal and building if
// the result is fewer cache misses and less
// memory use.
glm::vec3 vBoundriesBox[8];
// Points to the first child. We don't need
// to store 8 indices for the children if we
// can assume that all 8 children are stored
// contiguously in an array/vector. If the
// node is a leaf, this stores -1.
int32_t children;
// Points to the first element in this node
// or -1 if there are none.
int32_t first_element;
float combined_weight;
};
struct Octree
{
// Stores all the elements for the entire tree.
vector<OctreeElement> elements;
// Stores all the nodes for the entire tree. The
// first node is the root.
vector<OctreeNode> nodes;
};
struct-octreelement
{
//指向元素数据(点、三角形等)。
int32_t元件;
//指向下一个兄弟姐妹。
int32_t next;
};
结构八叉树
{
//这可以进一步减少到两个
//向量:一个盒子的中心和一半大小
//一点点算术仍然可以改进
//遍历和构建if的效率
//结果是更少的缓存未命中和更少的错误
//内存使用。
glm::vec3 vBoundriesBox[8];
//指向第一个孩子,我们不需要
//为孩子们存储8个索引,如果我们
//可以假定存储了所有8个子项
//在数组/向量中连续。如果
//节点是一个叶,它存储-1。
国际儿童;
//指向此节点中的第一个元素
//如果没有,则为-1。
int32_t第一_元素;
浮子组合重量;
};
结构八叉树
{
//存储整个树的所有元素。
矢量元素;
//存储整个树的所有节点
//第一个节点是根节点。
向量节点;
};
这一切都还很初级,还有很大的改进空间,我无法在一个答案中真正涵盖,但只是
void Octree::AddObject(Element* object)
{
this->objects.push_back(object);
}
struct OctreeElement
{
// Points to next sibling.
OctreeElement* next;
// Points to the element data (point, triangle, whatever).
Element* element;
};
struct OctreeNode
{
OctreeNode* children[8];
glm::vec3 vBoundriesBox[8];
// Points to the first element in this node
// or null if there are none.
OctreeElement* first_element;
float combined_weight;
bool leaf;
};
// Pop off element from parent.
OctreeElement* elt = parent->first_element;
parent->first_element = elt->next;
// Push it to the nth child.
elt->next = children[n];
children[n]->first_element = elt;
struct OctreeElement
{
// Points to the element data (point, triangle, whatever).
int32_t element;
// Points to next sibling.
int32_t next;
};
struct OctreeNode
{
// This can be further reduced down to two
// vectors: a box center and half-size. A
// little bit of arithmetic can still improve
// efficiency of traversal and building if
// the result is fewer cache misses and less
// memory use.
glm::vec3 vBoundriesBox[8];
// Points to the first child. We don't need
// to store 8 indices for the children if we
// can assume that all 8 children are stored
// contiguously in an array/vector. If the
// node is a leaf, this stores -1.
int32_t children;
// Points to the first element in this node
// or -1 if there are none.
int32_t first_element;
float combined_weight;
};
struct Octree
{
// Stores all the elements for the entire tree.
vector<OctreeElement> elements;
// Stores all the nodes for the entire tree. The
// first node is the root.
vector<OctreeNode> nodes;
};