C++如何实现轻量级性能计数器?(每秒调用次数统计)

1次阅读

用 std::atomic 实现每秒计数需避免锁、sleep 和 system_clock,采用 steady_clock 对齐整秒边界,用 exchange 原子获取并重置,注意缓存行伪共享,每个计数器单独 64 字节对齐。

C++如何实现轻量级性能计数器?(每秒调用次数统计)

std::atomic 做每秒计数,别碰锁

直接用 std::atomic<long long></long> 累加调用次数,比加互斥锁快一个数量级,且无上下文切换开销。线程fetch_add 是原子的,不需要额外同步。

常见错误是用普通 intlong 变量++,在高并发下会丢计数——这不是“偶尔不准”,而是必现问题。

  • 初始化用 std::atomic<long long> count{0}</long>,别用 int(溢出太快)
  • 每次调用只做 count.fetch_add(1, std::memory_order_relaxed)relaxed 足够,不需 acq_rel
  • 避免在 hot path 里调用 std::time()std::chrono::steady_clock::now(),它们本身有开销

每秒清零不能靠 sleep(1)

std::this_thread::sleep_for() 定时清零,会导致统计窗口漂移:比如上一秒从 1.2s 开始,下一秒就从 2.2s 开始,漏掉或重复统计部分请求。

正确做法是用单调时钟对齐整秒边界,靠时间戳差值判断是否跨秒。

立即学习C++免费学习笔记(深入)”;

  • 记录上次重置时间 last_reset_timestd::chrono::steady_clock::time_point
  • 每次读取计数前,先算当前秒数:auto now = steady_clock::now(); auto curr_sec = duration_cast<seconds>(now.time_since_epoch()).count();</seconds>
  • 如果 curr_sec != last_sec,才交换并重置计数器,同时更新 last_sec
  • 不要用 system_clock,它可能被系统管理员调整,导致秒数跳变

std::atomic::exchange() 是获取+重置的唯一安全方式

想拿到上一秒的计数值并清零,不能分两步:先 load()store(0)——中间可能被其他线程增加,造成漏计。

exchange() 是原子的“读旧值、写新值”操作,没有竞态窗口。

  • count.exchange(0, std::memory_order_relaxed) 一次性拿到上一秒累计值
  • 返回值就是你要上报的 QPS,原地归零,无需锁也不依赖顺序
  • 别用 compare_exchange_weak 循环模拟,纯属多此一举
  • 注意:返回的是 long long,别直接赋给 int 导致截断

小心 std::atomic 的内存序和缓存行伪共享

单个 std::atomic<long long></long> 没问题,但如果你把多个计数器(比如按接口名分组)塞进一个 Struct 里挨着放,不同线程频繁更新相邻字段,会触发 CPU 缓存行(64 字节)反复无效化,性能暴跌。

这不是理论风险,在千核服务器上实测过 QPS 掉 40% 以上。

  • 每个高频计数器之间至少 padding 64 字节,例如:alignas(64) std::atomic<long long> qps_a;</long>
  • 别把计数器和非原子字段(如字符串名、配置标志)混在一个 struct 里共用缓存行
  • 如果用数组存 N 个计数器,确保 sizeof(atomic) * N 不跨缓存行边界——更稳妥是每个单独 alignas(64)
  • Clang/GCC 下可用 __attribute__((aligned(64))) 强制对齐

真正难的不是写出来,是压测时发现 QPS 曲线毛刺多、波动大,最后追到缓存行争用上——这个点没人提,但线上最容易栽。

text=ZqhQzanResources