C++ 了解缓存友好、面向数据的对象和句柄
考虑实体/对象管理的传统OOP方法:C++ 了解缓存友好、面向数据的对象和句柄,c++,handle,data-oriented-design,C++,Handle,Data Oriented Design,考虑实体/对象管理的传统OOP方法: using namespace std; 看起来不错。但是如果std::vector需要重新分配其内部数组,则对实体的所有引用都将无效 解决方案是使用句柄类 struct Manager { vector<Entity> entities; // Cache-friendly void update() { /* erase-remove_if !alive entities */ } }; struct UserObject
using namespace std;
看起来不错。但是如果std::vector
需要重新分配其内部数组,则对实体的所有引用都将无效
解决方案是使用句柄类
struct Manager {
vector<Entity> entities; // Cache-friendly
void update() { /* erase-remove_if !alive entities */ }
};
struct UserObject {
// This reference may unexpectedly become invalid
Entity& entity;
};
结构实体{bool alive{true};};
结构实体句柄{int index;};
结构管理器{
向量实体;//缓存友好
void update(){/*擦除-remove_if!活动实体*/}
Entity&getEntity(entityhandleh){返回实体[h.index];}
};
结构用户对象{EntityHandle entity;};
如果我只是在向量的后面添加/删除实体,它似乎可以工作。我可以使用getEntity
方法检索所需的实体
但是如果我从向量的中间删除一个实体
,会怎么样?所有EntityHandle
实例现在都将持有不正确的索引,因为所有内容都已移位。例如:
句柄指向索引:2
实体A在更新过程中被删除() 现在句柄指向错误的实体
这个问题通常是如何处理的 句柄索引是否已更新 是否用占位符替换无效实体
澄清: 这些都是我所说的缓存友好设计的例子
此外,诸如Artemis之类的组件系统声称采用了线性缓存友好设计,它们使用的解决方案与手柄类似。他们是如何处理我在这个问题中描述的问题的?如果您确实已经测量到缓存位置为您提供了好处,那么我建议使用内存池方法:在最基本的级别上,如果您预先知道元素的最大数量,您可以简单地创建三个向量,一个是对象,一个具有活动对象指针,另一个具有自由对象指针。最初,自由列表具有指向元素容器中所有对象的指针,然后项目在激活时移动到活动列表,然后在删除时返回自由列表
即使从相应容器中添加/删除指针,对象也不会更改位置,因此引用也不会失效。要动态更改引用的向量实体,请修改设计,将索引存储在UserObject中,而不是直接指针中。通过这种方式,您可以更改引用向量,复制旧值,然后一切仍将正常工作。在缓存方面,单个指针的索引可以忽略不计,在指令方面也是如此
要处理删除,要么忽略它们(如果您知道有固定数量的删除),要么维护一个免费的索引列表。添加项目时使用此自由列表,然后仅在自由列表为空时增加向量。我有两种方法。第一种方法是在从容器中删除实体时更新句柄 第二种是使用键/值容器,如映射/哈希表,句柄必须包含键而不是索引 编辑: 第一个解决方案示例
struct Entity { bool alive{true}; };
struct EntityHandle { int index; };
struct Manager {
vector<Entity> entities; // Cache-friendly
void update() { /* erase-remove_if !alive entities */ }
Entity& getEntity(EntityHandle h) { return entities[h.index]; }
};
struct UserObject { EntityHandle entity; };
类管理器:
类实体{bool alive{true};};
类EntityHandle
{
公众:
EntityHandle(经理*经理)
{
经理->订阅(此);
//需要更多的索引代码吗
}
~EntityHandle(经理*经理)
{
经理->取消订阅(此);
}
无效更新(int-removedIndex)
{
如果(移除索引<索引)
{
--指数;
}
}
整数指数;
};
班级经理{
向量实体;//缓存友好
列表句柄;
布尔需要搬迁(const unique\u ptr&e)
{
bool result=!e->alive;
如果(结果)
用于(自动控制柄:控制柄)
{
句柄->更新(e->索引);
}
返回结果;
}
无效更新()
{
实体。删除(如果(开始(实体)、结束(实体)),
需要搬家);
}
Entity&getEntity(entityhandleh){返回实体[h.index];}
订阅(EntityHandle*handle)
{
把手。向后推(把手);
}
取消订阅(EntityHandle*handle)
{
//查找并删除
}
};
我希望这足以让你产生想法让我们回顾一下你的短语 缓存友好的线性内存
struct Entity { bool alive{true}; }
struct Manager {
vector<unique_ptr<Entity>> entities; // Non cache-friendly
void update() {
// erase-remove_if idiom: remove all !alive entities
entities.erase(remove_if(begin(entities), end(entities),
[](const unique_ptr<Entity>& e){ return !e->alive; }));
}
};
struct UserObject {
// Even if Manager::entities contents are re-ordered
// this reference is still valid (if the entity was not deleted)
Entity& entity;
};
“线性”的要求是什么?如果你真的有这样的要求,那么请参考@seano和@markb的答案。如果你不关心线性内存,那么我们开始吧
std::map
,std::set
,std::list
提供了对容器修改稳定(容忍)的迭代器-这意味着您可以保留迭代器,而不是保留引用:
class Manager:
class Entity { bool alive{true}; };
class EntityHandle
{
public:
EntityHandle(Manager *manager)
{
manager->subscribe(this);
// need more code for index
}
~EntityHandle(Manager *manager)
{
manager->unsubscribe(this);
}
void update(int removedIndex)
{
if(removedIndex < index)
{
--index;
}
}
int index;
};
class Manager {
vector<Entity> entities; // Cache-friendly
list<EntityHandle*> handles;
bool needToRemove(const unique_ptr<Entity>& e)
{
bool result = !e->alive;
if(result )
for(auto handle: handles)
{
handle->update(e->index);
}
return result;
}
void update()
{
entities.erase(remove_if(begin(entities), end(entities),
needToRemove);
}
Entity& getEntity(EntityHandle h) { return entities[h.index]; }
subscribe(EntityHandle *handle)
{
handles.push_back(handle);
}
unsubscribe(EntityHandle *handle)
{
// find and remove
}
};
关于std::list
的特别说明-在关于Bjarne Stroustrup的一些讲座中,不建议使用链表,但对于您的情况,您可以确保管理器中的实体
不会被修改-因此参考适用于此处
另外,谷歌快速搜索我还没有发现无序地图是否提供了稳定的迭代器,所以我上面的列表可能不完整
p.p.S发布后,我回忆起有趣的数据结构-分块列表。线性数组的链接列表-因此,您可以按链接顺序保留线性固定大小的分块。失眠症患者制作了一个很棒的powerpoint,他们的解决方案是这样的
struct UserObject {
// This reference may unexpectedly become invalid
my_container_t::iterator entity;
};
模板
班级资源经理
{
T数据[大小];
整数指数[大小];
回码;
ResourceManager():返回(0)
{
对于(size_t i=0;i我将重点讨论需要为向量设置可变大小的情况,例如,数据经常被插入,有时被清除。在这种情况下,在向量中使用虚拟数据或漏洞几乎与第一个解决方案中使用堆数据一样“糟糕”
如果您经常直接迭代所有数据,并且只使用很少的rand
template<typename T, size_t SIZE>
class ResourceManager
{
T data[SIZE];
int indices[SIZE];
size_t back;
ResourceManager() : back(0)
{
for(size_t i=0; i<SIZE; i++)
indices[i] = static_cast<int>(i);
}
int Reserve()
{ return indices[back++]; }
void Release(int handle)
{
for(size_t i=0; i<back; i++)
{
if(indices[i] == handle)
{
back--;
std::swap(indices[i], indices[back]);
return;
}
}
}
T GetData(size_t handle)
{ return data[handle]; }
};
#include <vector>
#include <map>
#include <algorithm>
#include <iostream>
#include <mutex>
using namespace std;
typedef __int64 EntityId;
template<class Entity>
struct Manager {
vector<Entity> m_entities; // Cache-friendly
map<EntityId, size_t> m_id_to_idx;
mutex g_pages_mutex;
public:
Manager() :
m_entities(),
m_id_to_idx(),
m_remove_counter(0),
g_pages_mutex()
{}
void update()
{
g_pages_mutex.lock();
m_remove_counter = 0;
// erase-remove_if idiom: remove all !alive entities
for (vector<Entity>::iterator i = m_entities.begin(); i < m_entities.end(); )
{
Entity &e = (*i);
if (!e.m_alive)
{
m_id_to_idx.erase(m_id_to_idx.find(e.m_id));
i = m_entities.erase(i);
m_remove_counter++;
return true;
}
else
{
m_id_to_idx[e.m_id] -= m_remove_counter;
i++;
}
}
g_pages_mutex.unlock();
}
Entity& getEntity(EntityId h)
{
g_pages_mutex.lock();
map<EntityId, size_t>::const_iterator it = m_id_to_idx.find(h);
if (it != m_id_to_idx.end())
{
Entity& et = m_entities[(*it).second];
g_pages_mutex.unlock();
return et;
}
else
{
g_pages_mutex.unlock();
throw std::exception();
}
}
EntityId inserEntity(const Entity& entity)
{
g_pages_mutex.lock();
size_t idx = m_entities.size();
m_id_to_idx[entity.m_id] = idx;
m_entities.push_back(entity);
g_pages_mutex.unlock();
return entity.m_id;
}
};
class Entity {
static EntityId s_uniqeu_entity_id;
public:
Entity (bool alive) : m_id (s_uniqeu_entity_id++), m_alive(alive) {}
Entity () : m_id (s_uniqeu_entity_id++), m_alive(true) {}
Entity (const Entity &in) : m_id(in.m_id), m_alive(in.m_alive) {}
EntityId m_id;
bool m_alive;
};
EntityId Entity::s_uniqeu_entity_id = 0;
struct UserObject
{
UserObject(bool alive, Manager<Entity>& manager) :
entity(manager.inserEntity(alive))
{}
EntityId entity;
};
int main(int argc, char* argv[])
{
Manager<Entity> manager;
UserObject obj1(true, manager);
UserObject obj2(false, manager);
UserObject obj3(true, manager);
cout << obj1.entity << "," << obj2.entity << "," << obj3.entity;
manager.update();
manager.getEntity(obj1.entity);
manager.getEntity(obj3.entity);
try
{
manager.getEntity(obj2.entity);
return -1;
}
catch (std::exception ex)
{
// obj 2 should be invalid
}
return 0;
}
typedef struct FreeList FreeList;
struct FreeList
{
/// Stores a pointer to the first block in the free list.
struct FlBlock* first_block;
/// Stores a pointer to the first free chunk.
struct FlNode* first_node;
/// Stores the size of a chunk.
int type_size;
/// Stores the number of elements in a block.
int block_num;
};
/// @return A free list allocator using the specified type and block size,
/// both specified in bytes.
FreeList fl_create(int type_size, int block_size);
/// Destroys the free list allocator.
void fl_destroy(FreeList* fl);
/// @return A pointer to a newly allocated chunk.
void* fl_malloc(FreeList* fl);
/// Frees the specified chunk.
void fl_free(FreeList* fl, void* mem);
// Implementation:
typedef struct FlNode FlNode;
typedef struct FlBlock FlBlock;
typedef long long FlAlignType;
struct FlNode
{
// Stores a pointer to the next free chunk.
FlNode* next;
};
struct FlBlock
{
// Stores a pointer to the next block in the list.
FlBlock* next;
// Stores the memory for each chunk (variable-length struct).
FlAlignType mem[1];
};
static void* mem_offset(void* ptr, int n)
{
// Returns the memory address of the pointer offset by 'n' bytes.
char* mem = ptr;
return mem + n;
}
FreeList fl_create(int type_size, int block_size)
{
// Initialize the free list.
FreeList fl;
fl.type_size = type_size >= sizeof(FlNode) ? type_size: sizeof(FlNode);
fl.block_num = block_size / type_size;
fl.first_node = 0;
fl.first_block = 0;
if (fl.block_num == 0)
fl.block_num = 1;
return fl;
}
void fl_destroy(FreeList* fl)
{
// Free each block in the list, popping a block until the stack is empty.
while (fl->first_block)
{
FlBlock* block = fl->first_block;
fl->first_block = block->next;
free(block);
}
fl->first_node = 0;
}
void* fl_malloc(FreeList* fl)
{
// Common case: just pop free element and return.
FlNode* node = fl->first_node;
if (node)
{
void* mem = node;
fl->first_node = node->next;
return mem;
}
else
{
// Rare case when we're out of free elements.
// Try to allocate a new block.
const int block_header_size = sizeof(FlBlock) - sizeof(FlAlignType);
const int block_size = block_header_size + fl->type_size*fl->block_num;
FlBlock* new_block = malloc(block_size);
if (new_block)
{
// If the allocation succeeded, initialize the block.
int j = 0;
new_block->next = fl->first_block;
fl->first_block = new_block;
// Push all but the first chunk in the block to the free list.
for (j=1; j < fl->block_num; ++j)
{
FlNode* node = mem_offset(new_block->mem, j * fl->type_size);
node->next = fl->first_node;
fl->first_node = node;
}
// Return a pointer to the first chunk in the block.
return new_block->mem;
}
// If we failed to allocate the new block, return null to indicate failure.
return 0;
}
}
void fl_free(FreeList* fl, void* mem)
{
// Just push a free element to the stack.
FlNode* node = mem;
node->next = fl->first_node;
fl->first_node = node;
}
struct Bucket
{
struct Node
{
// Stores the element data.
T some_data;
// Points to either the next node in the bucket
// or the next free node available if this node
// has been removed.
int next;
};
vector<Node> data;
// Points to first node in the bucket.
int head;
// Points to first free node in the bucket.
int free_head;
};
template <class T>
class ArrayWithHoles
{
private:
std::vector<T> elements;
std::stack<size_t> free_stack;
public:
...
size_t insert(const T& element)
{
if (free_stack.empty())
{
elements.push_back(element);
return elements.size() - 1;
}
else
{
const size_t index = free_stack.top();
free_stack.pop();
elements[index] = element;
return index;
}
}
void erase(size_t n)
{
free_stack.push(n);
}
};