C++ 事件系统 - 基于堆的事件触发
Posted
技术标签:
【中文标题】C++ 事件系统 - 基于堆的事件触发【英文标题】:C++ Event System - Heap based Event fire 【发布时间】:2021-11-26 22:32:42 【问题描述】:我已经开始了我的游戏引擎之旅,我计划让它成为多线程的。考虑到这一点和堆分配成本,我编写了一个 EventPool 类,负责缓存事件分配,从而减少堆分配,但它增加了搜索先前分配的相同类型的“空闲”事件指针的额外成本。理想情况下,您只想进行堆栈分配,但考虑到每个 EventType 都需要向下转换,我不确定这是否可行。
每个事件触发并删除每个帧或保留 EventPool 并从中搜索是否更好?
事件触发:
template<typename EventType>
static void Publish(const EventType& e)
const auto& handlerIt = m_Subscribers.find(typeid(EventType));
if (handlerIt == m_Subscribers.end())
return;
auto ev = EventPool::Allocate<EventType>(e);
for (auto& handler : handlerIt->second)
if (!ev->IsHandled())
ev->m_Handled = handler(ev);
ev->m_Handled = true;
如果 EventPool 方法更好,我该如何优化它?
EventPool 分配器:
template<class EventType>
static EventType* Allocate(const EventType& e)
const auto& poolIt = m_EventPool.find(typeid(EventType));
EventType* newEvent;
if (poolIt == m_EventPool.end())
newEvent = new EventType;
memcpy(newEvent, &e, sizeof(EventType));
m_EventPool[typeid(EventType)].push_back(newEvent);
return newEvent;
else
for (Event* ev : poolIt->second)
if (ev->IsHandled())
newEvent = static_cast<EventType*>(ev);
memcpy(newEvent, &e, sizeof(EventType));
return newEvent;
newEvent = new EventType;
memcpy(newEvent, &e, sizeof(EventType));
poolIt->second.push_back(newEvent);
return newEvent;
【问题讨论】:
do heap allocation per event fire and delete per frame
让我觉得你的所有事件都没有比游戏帧长(即使在游戏结束时未处理)。我说的对吗?
是的,至少在单线程上不行。在多线程系统中,事件的句柄可能会延迟
我错过了这个“(即使在它结束时未处理)”。答案是否定的,在处理之前不会被删除。
【参考方案1】:
根据
template<typename EventType> static void Publish(const EventType& e)
,编译器提前知道你的事件的所有类型(因为它必须为每个事件实例化Publish
模板函数)。您可以使用这个事实来完全避免运行时类型分派。例如:
template<typename E> using Subscriber = bool (*)(E const&); // or whatever it is
template<typename... Es> // all of the events which may be worked with go here
class EventPool
private:
std::tuple<std::vector<Es>...> storages; // a storage per event type
std::tuple<std::vector<Subscriber<Es>>...> subscribers; // a list of subscribers per event type
// or whatever you prefer over vectors
public:
template<typename E> void publish(E const& event)
get<E>(storages).push_back(event); // or whatever your (re)allocation policy is
for (auto& handler: get<Subscriber<E>>(subscribers))
if (event.handled = handler(event))
break;
;
现在所有 RTTI find(typeid(E))
都消失了 - 相反,您可以在编译时通过 std::get
在所有可能变体的元组上为您的事件类型选择适当的存储。此外,你不需要不安全的向下转型
newEvent = static_cast<EventType*>(ev)
因为只要不擦除类型就不需要恢复类型,就像在这种方法中一样。
谈到堆分配 - 您的事件不需要一个一个new
ed,这样可以为您节省大量分配。相反,为每种事件类型提供了一个存储(在我的例子中是动态数组 (std::vector
)),因此事件可以就地存储。
【讨论】:
【参考方案2】:数据结构
听起来您需要一个“池分配器”,这是一种特殊的分配器类型。我们还有“堆栈分配器”、“线性分配器”、“双端堆栈分配器”、“堆分配器”等。
这篇文章很好地概述了所涉及的内存操作: http://dmitrysoshnikov.com/compilers/writing-a-pool-allocator/
但是,这样的分配器仅在事件类型时起作用,即。 EventType
,具有可预测且未修改的大小(以字节为单位)。池分配器的实现依赖于一个数组,其中每个元素的内存是 reinterpret_cast
'ed 到 EvenType*
到下一个空闲项目。因此,它使用隐式空闲列表但没有单独的列表结构,因为列表包含在数据中。这是一个非常简单的代码来展示它是如何工作的:
// This static assert is very important!
static_assert(sizeof(EventType) >= sizeof(void*));
EvenType pool[5];
EventType* free_ptr = &pool[0];
for (int i = 0; i < 4; i++)
auto ptr = reinterpret_cast<EventType*>(&pool[i]);
*ptr = &pool[i+1];
auto ptr = reinterpret_cast<EventType*>(&pool[4]);
*ptr = nullptr;
// now allocate
EventType* new_ptr = nullptr;
if (free_ptr == nullptr)
// failed to allocate!
new_ptr = free_ptr;
free_ptr = reinterpret_cast<EventType*>(*free_ptr);
// Now, free_ptr should point to &pool[1]
这是一种关于未来发展的蓝图。这种代码很容易出错(尽管我已经做了很多,但我可能会犯一些错误),所以我强烈鼓励进行彻底的单元测试。
好处
听起来堆分配是您关心的问题,在运行时应该始终受到限制,尤其是在游戏引擎消息处理等时间关键型系统中。使用堆分配器,您可以预先分配一大块内存并在整个程序生命周期中使用该内存。因此,您需要提前知道在任何给定时刻,一次即时消息的最大数量。如果超过该数字,则消息将被丢弃,并且应将错误消息输出到记录器。
【讨论】:
以上是关于C++ 事件系统 - 基于堆的事件触发的主要内容,如果未能解决你的问题,请参考以下文章