仓库: gitee.com/liudegui/ev… v0.4.0 基准版本: eventpp v0.1.3 (wqking/eventpp) 平台: 跨平台 (ARM + x86) | C++14
一、根因分析
通过逐行阅读 eventpp v0.1.3 核心头文件,定位到 6 个性能瓶颈:
| # | 根因 | 位置 | 严重程度 |
|---|---|---|---|
| 1 | CallbackList 遍历时每个节点都加锁 | callbacklist.h doForEachIf | 致命 |
| 2 | EventQueue enqueue 双锁 (freeListMutex + queueListMutex) | eventqueue.h doEnqueue | 高 |
| 3 | EventDispatcher dispatch 时加排他锁查 map | eventdispatcher.h | 高 |
| 4 | SpinLock 无 YIELD 指令,纯烧 CPU | eventpolicies.h | 中 |
| 5 | std::list 每节点堆分配,cache 不友好 | eventpolicies_i.h | 中 |
| 6 | 无 cache-line 对齐,多核 false sharing | 全局 | 中 |
二、优化方案 (OPT-1 ~ OPT-15)
OPT-1/11: SpinLock 指数退避 [Batch 1 -> Batch 5]
v0.2.0 初版添加 CPU hint (OPT-1),v0.4.0 升级为指数退避 (OPT-11),高竞争场景下显著降低总线流量:
void lock() {
// Fast path: no contention
if(!locked.test_and_set(std::memory_order_acquire)) {
return;
}
// Slow path: exponential backoff
unsigned backoff = 1;
while(locked.test_and_set(std::memory_order_acquire)) {
for(unsigned i = 0; i < backoff; ++i) {
#if defined(__aarch64__) || defined(__arm__)
__asm__ __volatile__("yield");
#elif defined(__x86_64__) || defined(_M_X64) || defined(__i386__)
__builtin_ia32_pause();
#endif
}
if(backoff < kMaxBackoff) {
backoff <<= 1; // 1 -> 2 -> 4 -> ... -> 64
}
}
}
static constexpr unsigned kMaxBackoff = 64;
OPT-6/10: Cache-Line 对齐 [Batch 1 -> Batch 5]
对 EventQueue 热成员进行 cache-line 隔离,消除 false sharing。v0.2.0 初版硬编码 64B (OPT-6),v0.4.0 升级为平台自适应 (OPT-10):
// OPT-10: 平台自适应 cache-line 大小
#ifndef EVENTPP_CACHELINE_SIZE
#if defined(__APPLE__) && defined(__aarch64__)
#define EVENTPP_CACHELINE_SIZE 128 // Apple Silicon (M1/M2/M3)
#else
#define EVENTPP_CACHELINE_SIZE 64 // x86 / Cortex-A
#endif
#endif
#define EVENTPP_ALIGN_CACHELINE alignas(EVENTPP_CACHELINE_SIZE)
EVENTPP_ALIGN_CACHELINE mutable ConditionVariable queueListConditionVariable;
EVENTPP_ALIGN_CACHELINE mutable Mutex queueListMutex;
EVENTPP_ALIGN_CACHELINE Mutex freeListMutex;
OPT-7: 内存序降级 [Batch 1]
CounterGuard 从 seq_cst 降级为 acq_rel/release。ARM 上避免额外的 dmb ish 全屏障:
struct CounterGuard {
explicit CounterGuard(T & v) : value(v) {
value.fetch_add(1, std::memory_order_acq_rel);
}
~CounterGuard() {
value.fetch_sub(1, std::memory_order_release);
}
};
OPT-2: CallbackList 批量预取 [Batch 2] -- 核心改动
原始代码每访问一个 node->next 都加锁(N 个回调 = N 次 mutex),这是 128us 最大延迟的根因。
改为批量预取:一次加锁读取 8 个节点,无锁遍历批次,再加锁取下一批。锁操作减少约 8 倍。
static constexpr size_t kBatchSize = 8;
NodePtr batch[kBatchSize];
while(node) {
size_t count = 0;
{
std::lock_guard<Mutex> lockGuard(mutex); // 每 8 个节点锁一次
NodePtr cur = node;
while(cur && count < kBatchSize) { batch[count++] = cur; cur = cur->next; }
}
for(size_t i = 0; i < count; ++i) { /* 无锁执行回调 */ }
{ std::lock_guard<Mutex> lockGuard(mutex); node = batch[count - 1]->next; }
}
最初尝试"一次快照全部节点",但破坏了重入 append 语义(counter overflow 测试失败)。批量预取保留了重入语义。
OPT-3: EventDispatcher 读写锁分离 [Batch 3]
dispatch(高频)用读锁,appendListener(低频)用写锁,多线程 dispatch 不再互斥:
using SharedMutex = std::shared_timed_mutex; // C++14
// dispatch: std::shared_lock<SharedMutex> (读锁)
// append: std::unique_lock<SharedMutex> (写锁)
OPT-4: doEnqueue try_lock [Batch 4]
freeListMutex 改为 try_lock,竞争时跳过回收直接分配新节点,不阻塞热路径。外层无锁预检查避免不必要的锁操作:
if(! freeList.empty()) { // 无锁预检查
std::unique_lock<Mutex> lock(freeListMutex, std::try_to_lock);
if(lock.owns_lock() && !freeList.empty()) {
tempList.splice(tempList.end(), freeList, freeList.begin());
}
}
OPT-8: waitFor 自适应 Spin [Batch 4]
四阶段等待:快速检查 -> CPU hint spin (128 次) -> 让出时间片 (16 次) -> 回退到 CV wait:
if(doCanProcess()) return true; // Phase 1: 快速检查
for(int i = 0; i < 128; ++i) { // Phase 2: spin + CPU hint (~0.5-2us)
if(doCanProcess()) return true;
/* yield / pause */
}
for(int i = 0; i < 16; ++i) { // Phase 3: 让出时间片 (~2-20us)
if(doCanProcess()) return true;
std::this_thread::yield();
}
return cv.wait_for(lock, duration, ...); // Phase 4: CV wait (futex)
OPT-5/9: PoolAllocator 池化分配器 [Batch 5]
静态 per-type 池化分配器,通过 Policy 机制 opt-in。保留 splice() 兼容性(14 处调用):
struct MyPolicies {
template <typename T>
using QueueList = eventpp::PoolQueueList<T, 4096>;
};
eventpp::EventQueue<int, void(const Payload&), MyPolicies> queue;
关键设计:静态单例池 -> operator== 恒 true -> splice() 安全;多 slab 动态增长 (OPT-9a),无锁 CAS free list (OPT-9b),仅 grow() 冷路径使用 SpinLock。
// OPT-14: 一站式高性能策略预设
struct HighPerfPolicy {
template <typename T>
using QueueList = eventpp::PoolQueueList<T, 8192>;
using Threading = eventpp::GeneralThreading<SpinLock>;
};
eventpp::EventQueue<int, void(const Payload&), HighPerfPolicy> queue;
OPT-15: processQueueWith 零开销访问者分发 [Batch 6]
OPT-1 ~ OPT-14 优化了锁策略、内存分配和缓存布局,但 process() 的分发热路径仍经过 5 层间接调用:
process()
for each queued event:
doDispatchQueuedEvent() // tuple 解包
directDispatch(event, args...) // EventDispatcher 入口
shared_lock<SharedMutex> // listenerMutex 读锁
map.find(event) // 事件 ID -> CallbackList 查找
CallbackList::operator()() // 回调链表调用
doForEachIf() // 批量预取遍历 (每 8 节点加锁)
shared_ptr<Node> traversal // 引用计数开销
std::function(args...) // 类型擦除间接调用
对于单消费者场景 (一个线程消费所有事件,事件处理逻辑编译期已知),上述基础设施开销完全不必要:
- 不需要 shared_lock -- 只有一个消费者
- 不需要 map.find -- 消费者已知如何处理所有事件
- 不需要 CallbackList -- 不需要动态注册/注销回调
- 不需要 std::function -- 处理函数编译期已知
newosp 项目的 ProcessBatchWith<Visitor> 已验证此优化方向: 绕过 FixedFunction 回调表 + SharedSpinLock,使用 std::visit 编译期跳转表 (C++17),实测 15x 加速 (2 ns/msg vs 30 ns/msg)。
API 设计
processQueueWith -- 处理队列中的所有事件:
template <typename Visitor>
bool processQueueWith(Visitor && visitor);
- 每个事件直接调用
visitor(event, args...)-- 无间接调用 - 返回
true如果处理了至少一个事件
processOneWith -- 处理队列中的一个事件:
template <typename Visitor>
bool processOneWith(Visitor && visitor);
- 返回
true如果处理了一个事件
Visitor 协议: Visitor 接收的第一个参数为事件 ID (EventType),后续参数与 EventQueue 原型签名中的 Args... 相同。
// 函数对象 (推荐: 编译器可内联)
struct MyVisitor {
void operator()(int event, const std::string & data) {
switch(event) {
case EVENT_SENSOR: handleSensor(data); break;
case EVENT_MOTOR: handleMotor(data); break;
}
}
};
// Lambda
queue.processQueueWith([](int event, const std::string & data) {
// ...
});
热路径对比
| 开销项 | process() | processQueueWith() |
|---|---|---|
shared_lock<SharedMutex> (listenerMutex) | 每条消息 | 无 |
map.find(event) 查找 | 每条消息 | 无 |
| CallbackList mutex (每 8 节点加锁) | 每批次 | 无 |
shared_ptr<Node> 链表遍历 | 每个回调 | 无 |
std::function 间接调用 | 每个回调 | 无 |
| Mixin beforeDispatch 检查 | 每条消息 | 无 |
| Visitor 直接调用 (可内联) | -- | 每条消息 |
共享的基础设施 (无差异):
- 队列 swap (lock_guard + std::swap) -- 两者相同
- CounterGuard (emptyQueue 语义) -- 两者相同
- BufferedItem clear + freeList 回收 -- 两者相同
核心实现
// eventqueue.h, EventQueueBase 类内
template <typename Visitor>
bool processQueueWith(Visitor && visitor)
{
if(! queueList.empty()) {
BufferedItemList tempList;
CounterGuard<decltype(queueEmptyCounter)> counterGuard(queueEmptyCounter);
{
std::lock_guard<Mutex> queueListLock(queueListMutex);
std::swap(queueList, tempList);
}
if(! tempList.empty()) {
for(auto & item : tempList) {
doVisitQueuedEvent(
visitor,
item.get(),
typename MakeIndexSequence<sizeof...(Args)>::Type()
);
item.clear();
}
std::lock_guard<Mutex> queueListLock(freeListMutex);
freeList.splice(freeList.end(), tempList);
return true;
}
}
return false;
}
// Helper: tuple 解包 + visitor 直接调用
template <typename V, typename T, size_t ...Indexes>
void doVisitQueuedEvent(V && visitor, T && item, IndexSequence<Indexes...>)
{
visitor(item.event, std::get<Indexes>(item.arguments)...);
}
C++14 兼容性
| 特性 | C++17 (newosp) | C++14 (eventpp) |
|---|---|---|
| 分发机制 | std::visit + std::variant | visitor(event, args...) 直接调用 |
| 参数展开 | fold expression | IndexSequence + pack expansion |
| 条件编译 | if constexpr | SFINAE / enable_if |
| 索引序列 | std::index_sequence | eventpp 自有 MakeIndexSequence |
eventpp 的 EventQueue 是同构的 (所有事件共享相同回调签名),不需要 variant/visit。 Visitor 接收的参数类型在编译期由模板参数确定,天然 C++14 兼容。
与 process() 的关系
- processQueueWith 是
process()的替代品,不是叠加使用 - 两者消费同一个队列 (queueList)
- 适用于单消费者 (MPSC) 场景
- 如需多消费者或动态注册回调,仍使用
process()
使用场景: 单消费者事件循环
eventpp::EventQueue<int, void(int, const SensorData &)> queue;
struct EventHandler {
void operator()(int event, int id, const SensorData & data) {
switch(event) {
case SENSOR_UPDATE: processSensor(id, data); break;
case MOTOR_CMD: executeMotor(id, data); break;
}
}
};
// 事件循环 -- 零开销分发
EventHandler handler;
while(running) {
queue.processQueueWith(handler);
}
与 newosp ProcessBatchWith 的对照
| 维度 | newosp | eventpp |
|---|---|---|
| 队列 | Lock-free MPSC ring buffer | std::list + swap |
| 类型系统 | std::variant (异构) | 同构 (相同签名) |
| 分发 | std::visit 跳转表 | visitor(event, args...) 直接调用 |
| C++ 标准 | C++17 | C++14 |
| 回调模式 | FixedFunction + callback_table | std::function + CallbackList |
| 绕过的层 | SharedSpinLock + callback遍历 + FixedFunction | SharedMutex + map.find + CallbackList + std::function |
三、性能数据
测试环境:Ubuntu 24.04, GCC 13.3, -O3 -march=native
Raw EventQueue (1M 消息)
| 指标 | 优化前 (v0.1.3) | 优化后 | 变化 |
|---|---|---|---|
| 吞吐量 | 22.2 M/s | 24.8 M/s | +12% |
| 入队延迟 | 46 ns | 42 ns | -9% |
Active Object 模式(多线程)
| 指标 | 优化前 | 优化后 | 提升 |
|---|---|---|---|
| 吞吐量 (10K) | ~1.6 M/s | 8.5 M/s | 5.3x |
| 持续吞吐 (5s) | ~1.25 M/s | 3.1 M/s | 2.5x |
| E2E P50 | ~1,200 ns | 11,588 ns | 吞吐-延迟权衡 |
| E2E P99 | ~8,953 ns | 24,289 ns | 吞吐-延迟权衡 |
PoolQueueList (OPT-5, 10K 消息)
| 方案 | 吞吐量 | 入队延迟 |
|---|---|---|
| std::list (默认) | 22.2 M/s | 46 ns |
| PoolQueueList | 28.5 M/s | 36 ns |
processQueueWith (OPT-15, CPU pinned to core 1)
| 场景 | process() | processQueueWith() | 加速比 |
|---|---|---|---|
| 单事件 ID, 100K 消息 | 152.4 ns/msg | 9.1 ns/msg | 16.7x |
| 10 个事件 ID, 100K 消息 | 151.5 ns/msg | 10.0 ns/msg | 15.2x |
| 10 个事件 ID, 1M 消息 | 76.6 ns/msg | 21.2 ns/msg | 3.6x |
分析:
- 100K 消息场景加速比约 15-17x,与 newosp 的 15x 加速一致
- 1M 消息场景加速比降至 3.6x,因为大队列下 std::list 的 freeList 回收成为共同瓶颈
- 中位数 (P50) 更能反映稳态性能: 6.1 ns/msg vs 154.9 ns/msg = 25x
资源消耗
| 指标 | 优化前 | 优化后 | 变化 |
|---|---|---|---|
| 测试套件时间 | ~23 s | ~18 s | -22% |
| 峰值内存 | 113 MB | 113 MB | 不变 |
| 上下文切换 | ~90 | 84 | -7% |
四、设计决策
| 问题 | 选择 | 原因 |
|---|---|---|
| OPT-2: 快照 vs 批量预取 | 批量预取 (8 节点) | 快照破坏重入 append 语义 |
| OPT-3: shared_mutex vs 无锁 map | shared_mutex | 改动小,C++14 兼容 |
| OPT-5/9: Ring Buffer vs Pool Allocator | Pool Allocator + 多 slab + CAS | Ring Buffer 不支持 splice()(14 处调用);CAS 无锁 free list 消除热路径锁竞争 |
| OPT-15: process() vs processQueueWith | processQueueWith 作为替代 API | 单消费者场景不需要 5 层间接调用基础设施;C++14 天然兼容 |
五、修改文件
| 文件 | 涉及 OPT |
|---|---|
include/eventpp/eventpolicies.h | OPT-1, OPT-3, OPT-6, OPT-10, OPT-11 |
include/eventpp/callbacklist.h | OPT-2 |
include/eventpp/eventdispatcher.h | OPT-3 |
include/eventpp/hetereventdispatcher.h | OPT-3 |
include/eventpp/eventqueue.h | OPT-4, OPT-6, OPT-8, OPT-14, OPT-15 |
include/eventpp/internal/eventqueue_i.h | OPT-7 |
include/eventpp/internal/poolallocator_i.h | OPT-5, OPT-9 (新增) |
六、验证体系
| 验证项 | 方法 | 通过标准 |
|---|---|---|
| 编译 | cmake --build . --target unittest | 零错误 |
| 功能 | ctest (410 个测试用例) | 410/410 PASS |
| processQueueWith | 10 项专项测试 (218 assertions) | 10/10 PASS |
| 线程安全 | -fsanitize=thread | 无新增 data race |
| 内存安全 | -fsanitize=address + detect_leaks=1 | 零错误零泄漏 |
| 性能 | eventpp_raw_benchmark | 无回退 >5% |
processQueueWith 测试覆盖:
- 基本分发、多事件全量处理、空队列返回 false
- 事件顺序保持、processOneWith 单事件/剩余保留
- 自定义 Policy (SingleThreading) 兼容
- processQueueWith vs process 结果一致性
- 非整型事件 ID (std::string)、复杂参数 (多参数、移动语义)
cd refs/eventpp/tests && mkdir -p build && cd build
cmake .. -DCMAKE_BUILD_TYPE=Release
cmake --build . --target unittest -j$(nproc)
ctest --output-on-failure
七、致谢
- wqking/eventpp -- 原始库
- iceoryx -- PoolAllocator 设计灵感
- newosp -- ProcessBatchWith Visitor 模式验证