上一篇我們講到了AI架構之一的行為樹,本篇文章和下一篇文章我們將對行為樹進行優化,在本篇文章中我們講到的是內存優化
問題##
上一篇中我們設計的行為樹由於直接采用new進行動態內存分配,沒有自己進行管理。因此行為樹各節點的存儲位置會散布在內存空間的各處,行為樹在不同節點中切換時會導致Cache頻繁失效。
通過內存管理改變行為樹節點的內存分布,可以顯著提高行為樹的內存性能。
解決辦法##
我們可以在BehaviorTree中引入一組內存分配的API來保證各節點盡量分配在連續的內存上,代碼如下
BehaviorTree(Behavior*InRoot):Root(InRoot),
Buffer(new uint8_t[MaxBehaviorTreeMemory]),Offset(0){}
~BehaviorTree(){ delete[] Buffer; }
template<typename T>
T* Allocate()
{
T* Node = new((void*)((uintptr_t)Buffer + Offset)) T;
Offset += sizeof(T);
assert(Offset < MaxBehaviorTreeMemory);
return Node;
}
我們在BehaviorTree中引入一個Allocate函數用來負責所有節點的內存分配。
當行為樹被構造時,一塊用於保存節點的內存空間Bufffer會隨之分配,Allocate函數通過Placement new在Buffer上進行內存分配,通過Offset記錄分配已分配內存的偏移地址。
通過這種方式我們可以讓節點分配在連續的內存上,同時通過控制分配節點的順序(如深度遍歷廣度遍歷等),我們可以進一步減少行為樹遍歷時產生的Cache失效,提高內存性能。)
復合節點
除了對節點分配進行優化,我們還可以改變復合節點的內存布局,進一步提升性能。
class Composite :public Behavior
{
public:
friend class BehaviorTree;
virtual void AddChild(Behavior* InChild) override
{
assert(ChildrenCount < MaxChildrenPerComposite);
ptrdiff_t p = (uintptr_t)InChild - (uintptr_t)this;
assert(p < std::numeric_limits<uint16_t>::max());
Children[ChildrenCount++] = static_cast<uint16_t>(p);
}
Behavior* GetChild(size_t index)
{
assert(index < MaxChildrenPerComposite);
return (Behavior*)((uintptr_t)this + Children[index]);
}
size_t GetChildrenCount()
{
return ChildrenCount;
}
protected:
uint16_t Children[MaxChildrenPerComposite];
uint16_t ChildrenCount = 0;
};
在如上代碼中,我們通過靜態數組代替vector,避免在存儲時vector所產生的額外堆操作,通過用保存子節點相對於復合節點的偏移地址來代替直接保存子節點指針以節省內存空間。由於更換了子節點的存儲方式,我們需要通過getchild()函數來根據復合節點地址和子節點偏移地址獲得子節點指針。
總結
以上,就是關於行為樹的內存優化方式,當然凡事無絕對,究竟如何構造行為樹應當根據實際情況選擇,下一篇我們將講述另一種行為樹優化方法。
[gihub鏈接][1]