eventpp 性能优化实战: 6 个瓶颈定位与 5 倍吞吐提升

1 阅读9分钟

仓库: gitee.com/liudegui/ev… v0.4.0 基准版本: eventpp v0.1.3 (wqking/eventpp) 平台: 跨平台 (ARM + x86) | C++14


一、根因分析

通过逐行阅读 eventpp v0.1.3 核心头文件,定位到 6 个性能瓶颈:

#根因位置严重程度
1CallbackList 遍历时每个节点都加锁callbacklist.h doForEachIf致命
2EventQueue enqueue 双锁 (freeListMutex + queueListMutex)eventqueue.h doEnqueue
3EventDispatcher dispatch 时加排他锁查 mapeventdispatcher.h
4SpinLock 无 YIELD 指令,纯烧 CPUeventpolicies.h
5std::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]

CounterGuardseq_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::variantvisitor(event, args...) 直接调用
参数展开fold expressionIndexSequence + pack expansion
条件编译if constexprSFINAE / enable_if
索引序列std::index_sequenceeventpp 自有 MakeIndexSequence

eventpp 的 EventQueue 是同构的 (所有事件共享相同回调签名),不需要 variant/visit。 Visitor 接收的参数类型在编译期由模板参数确定,天然 C++14 兼容。

与 process() 的关系

  • processQueueWithprocess()替代品,不是叠加使用
  • 两者消费同一个队列 (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 的对照

维度newospeventpp
队列Lock-free MPSC ring bufferstd::list + swap
类型系统std::variant (异构)同构 (相同签名)
分发std::visit 跳转表visitor(event, args...) 直接调用
C++ 标准C++17C++14
回调模式FixedFunction + callback_tablestd::function + CallbackList
绕过的层SharedSpinLock + callback遍历 + FixedFunctionSharedMutex + map.find + CallbackList + std::function

三、性能数据

测试环境:Ubuntu 24.04, GCC 13.3, -O3 -march=native

Raw EventQueue (1M 消息)

指标优化前 (v0.1.3)优化后变化
吞吐量22.2 M/s24.8 M/s+12%
入队延迟46 ns42 ns-9%

Active Object 模式(多线程)

指标优化前优化后提升
吞吐量 (10K)~1.6 M/s8.5 M/s5.3x
持续吞吐 (5s)~1.25 M/s3.1 M/s2.5x
E2E P50~1,200 ns11,588 ns吞吐-延迟权衡
E2E P99~8,953 ns24,289 ns吞吐-延迟权衡

PoolQueueList (OPT-5, 10K 消息)

方案吞吐量入队延迟
std::list (默认)22.2 M/s46 ns
PoolQueueList28.5 M/s36 ns

processQueueWith (OPT-15, CPU pinned to core 1)

场景process()processQueueWith()加速比
单事件 ID, 100K 消息152.4 ns/msg9.1 ns/msg16.7x
10 个事件 ID, 100K 消息151.5 ns/msg10.0 ns/msg15.2x
10 个事件 ID, 1M 消息76.6 ns/msg21.2 ns/msg3.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 MB113 MB不变
上下文切换~9084-7%

四、设计决策

问题选择原因
OPT-2: 快照 vs 批量预取批量预取 (8 节点)快照破坏重入 append 语义
OPT-3: shared_mutex vs 无锁 mapshared_mutex改动小,C++14 兼容
OPT-5/9: Ring Buffer vs Pool AllocatorPool Allocator + 多 slab + CASRing Buffer 不支持 splice()(14 处调用);CAS 无锁 free list 消除热路径锁竞争
OPT-15: process() vs processQueueWithprocessQueueWith 作为替代 API单消费者场景不需要 5 层间接调用基础设施;C++14 天然兼容

五、修改文件

文件涉及 OPT
include/eventpp/eventpolicies.hOPT-1, OPT-3, OPT-6, OPT-10, OPT-11
include/eventpp/callbacklist.hOPT-2
include/eventpp/eventdispatcher.hOPT-3
include/eventpp/hetereventdispatcher.hOPT-3
include/eventpp/eventqueue.hOPT-4, OPT-6, OPT-8, OPT-14, OPT-15
include/eventpp/internal/eventqueue_i.hOPT-7
include/eventpp/internal/poolallocator_i.hOPT-5, OPT-9 (新增)

六、验证体系

验证项方法通过标准
编译cmake --build . --target unittest零错误
功能ctest (410 个测试用例)410/410 PASS
processQueueWith10 项专项测试 (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

七、致谢