现在几天我正试图加速我的Force-Directed图形实现。到目前为止,我已经实现了Barnes-Hut算法,该算法使用八叉树来减少计算次数。我已多次测试它,并且与力相关的计算数量确实大幅减少。下面是没有Barns-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;
}
}
}
我在这里做的是浏览图表中的所有元素并将它们添加到树根。另外,我正在扩展表示我的八叉树边框的框,以便进行下一个循环,因此所有节点都适合内部。
对八叉树结构更新重要的字段如下:
Octree* trees[2][2][2];
glm::vec3 vBoundriesBox[8];
bool leaf;
float combined_weight = 0;
std::vector<Element*> objects;
负责更新的部分代码:
#define MAX_LEVELS 5
void Octree::AddObject(Element* object)
{
this->objects.push_back(object);
}
void Octree::Update()
{
if(this->objects.size()<=1 || level > MAX_LEVELS)
{
for(Element* Element:this->objects)
{
Element->parent_group = this;
}
return;
}
if(leaf)
{
GenerateChildren();
leaf = false;
}
while (!this->objects.empty())
{
Element* obj = this->objects.back();
this->objects.pop_back();
if(contains(trees[0][0][0],obj))
{
trees[0][0][0]->AddObject(obj);
trees[0][0][0]->combined_weight += obj->weight;
} else if(contains(trees[0][0][1],obj))
{
trees[0][0][1]->AddObject(obj);
trees[0][0][1]->combined_weight += obj->weight;
} else if(contains(trees[0][1][0],obj))
{
trees[0][1][0]->AddObject(obj);
trees[0][1][0]->combined_weight += obj->weight;
} else if(contains(trees[0][1][1],obj))
{
trees[0][1][1]->AddObject(obj);
trees[0][1][1]->combined_weight += obj->weight;
} else if(contains(trees[1][0][0],obj))
{
trees[1][0][0]->AddObject(obj);
trees[1][0][0]->combined_weight += obj->weight;
} else if(contains(trees[1][0][1],obj))
{
trees[1][0][1]->AddObject(obj);
trees[1][0][1]->combined_weight += obj->weight;
} else if(contains(trees[1][1][0],obj))
{
trees[1][1][0]->AddObject(obj);
trees[1][1][0]->combined_weight += obj->weight;
} else if(contains(trees[1][1][1],obj))
{
trees[1][1][1]->AddObject(obj);
trees[1][1][1]->combined_weight += obj->weight;
}
}
for(int i=0;i<2;i++)
{
for(int j=0;j<2;j++)
{
for(int k=0;k<2;k++)
{
trees[i][j][k]->Update();
}
}
}
}
bool Octree::contains(Octree* child, Element* object)
{
if(object->pos[0] >= child->vBoundriesBox[0][0] && object->pos[0] <= child->vBoundriesBox[1][0] &&
object->pos[1] >= child->vBoundriesBox[4][1] && object->pos[1] <= child->vBoundriesBox[0][1] &&
object->pos[2] >= child->vBoundriesBox[3][2] && object->pos[2] <= child->vBoundriesBox[0][2])
return true;
return false;
}
因为我使用指针移动树元素,所以我不认为对象创建/破坏是一个问题。我认为可能对速度产生影响的一个地方是:
Element* obj = this->objects.back();
this->objects.pop_back();
if(contains(trees[0][0][0],obj))
虽然我不确定如何能够省略/加快速度。有人有什么建议吗?
修改
我已经完成了一些餐巾纸数学运算,我想还有一个地方可能会导致主要的速度降低。检查Update
方法的边界看起来很多,我计算的是由于这种情况而增加的复杂性是在最坏的情况下:
number_of_elements * number_of_childern * number_of_faces * MAX_LEVELS
在我的情况下等于number_of_elements * 240。
有人可以确认我的想法是否合理吗?
答案 0 :(得分:2)
如果我理解正确,你是否在每个单独的八叉树节点中存储一个指针向量?
std::vector<Element*> objects;
...
void Octree::AddObject(Element* object)
{
this->objects.push_back(object);
}
正如我从这段代码中所理解的那样,对于八叉树构建,你的父节点pop_back
元素指针来自父矢量,并开始推回以将适当的元素传递给子节点。
如果是这样的话,我可以立刻说这是一个主要的瓶颈,甚至没有测量,因为我之前已经处理过这样的八叉树实施,并且将建筑物改进了10倍以上并且减少了通过简单地使用单链表来缓存在遍历中的缺失,在这种特定情况下,与小船vectors
(每个节点一个)相比较,显着减少了所涉及的堆分配/解除分配,甚至改善了空间局部性)。我并不是说它是唯一的瓶颈,但它绝对是一个重要的瓶颈。
如果情况确实如此,我建议这样做:
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
来存储所有元素(整个树的所有元素,而不是每个节点一个向量),并使用索引将元素链接到该向量中而不是指针。如果您使用索引而不是链接列表的指针,则可以连续存储所有节点,而无需使用内存分配器,只需使用一个大的旧vector
来存储所有内容,同时将链接的内存需求减半(假设为64)如果您可以使用索引,那么位指针和32位索引就足够了。
如果您使用32位索引,您可能也不需要所有32位,此时您可以使用31位并填充leaf
布尔值,这会增加大量的将节点(大约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;
};
这一切仍然非常简陋,而且我在一个答案中真的可以覆盖那么多的改进空间,但只是做这些事情应该已经有很多帮助,从避免开始每个节点单独vector
作为您最大的改进。
减少堆分配和改进参考位置的链接列表
我觉得我以前和很多C ++开发人员一起忘记了或者从未学过这些东西,但链接列表并不总是转换为增加的堆分配和缓存遗漏,特别是当每个节点不需要单独的堆分配时。如果比较点是少量向量,那么链表实际上会减少缓存未命中数并减少堆分配。拿这个基本的例子:
让我们说实际网格有10,000个单元格。在这种情况下,每个单元存储一个32位索引并使用存储在一个大数组(或一个大vector
)中的32位索引将元素链接在一起将会更便宜并且需要更少的内存分配(以及通常少得多的内存)比存储10,000个向量。 Vector是一种用于存储非平凡数据量的优秀结构,但它并不是您想用来存储大量可变大小的列表的东西。单链表可能已经是一个重大的改进,它们非常适合于在恒定时间和廉价的情况下将元素从一个列表传输到另一个列表,因为这只需要操作3个指针(或3个索引)额外的分支。
因此链接列表仍然有很多用途。当您以减少堆积分配而非增加堆积分配的方式实际使用它们时,它们特别有用。