1. 线程模型与任务调度
线程池设计
固定大小 vs 弹性伸缩:根据硬件核数固定线程数,避免过度切换;对突发负载可动态扩缩容。
任务队列类型:单队列加全局锁 → 多生产者/多消费者无锁队列 → 工作窃取(work-stealing)队列,可显著提升并发度。
线程本地分配(Thread‐local allocator):为每个线程维护独立的内存池,减少全局分配器的锁竞争。
实战示例:基于
**std::thread**
+ 工作窃取
// 简化版:每个 worker 都有自己的双端队列
class WorkStealingQueue {
// … push(), pop(), steal() 实现略 …
};
std::vector<WorkStealingQueue> queues;
void worker(int id) {
while (running) {
Task t;
if (!queues[id].pop(t)) {
// 从其他队列窃取
for (int i = 0; i < N; ++i)
if (queues[i].steal(t)) break;
}
if (t) t();
}
}
2. 内存管理与函数调用优化
减少函数调用开销
使用
inline
给热点函数内联;但注意过度内联可能增大代码体积,导致 I-cache Miss。避免虚函数和多态:虚函数调用会通过 vtable 间接跳转,开销较大;在性能关键路径尽量使用模板或 CRTP(Curiously Recurring Template Pattern)实现静态多态。
批量处理:将多次逻辑拆分的函数调用合并成一次批量处理,减少函数调用次数。
自定义分配器 & 对象池
对短生命周期小对象(如任务结构体)采用对象池、内存池(arena),避免多线程环境下频繁调用
new
/delete
所带来的加锁/页表操作开销。栈分配优先:能用局部(栈)对象就不堆分配,栈分配和销毁成本远小于堆。
tcmalloc
、jemalloc
或者自己基于boost::pool
实现。
示例:简单对象池
template<typename T>
class ObjectPool {
std::vector<T*> free_list;
public:
T* alloc() {
if (free_list.empty())
return static_cast<T*>(::operator new(sizeof(T)));
T* p = free_list.back();
free_list.pop_back();
return p;
}
void dealloc(T* p) { free_list.push_back(p); }
};
3. 原子操作与无锁编程
用
**std::atomic**
代替原始变量 + 锁原理:
std::atomic<T>
提供无锁的原子操作(在大多数平台上是 CPU 原生指令),避免了使用std::mutex
带来的内核态上下文切换和锁排队。合理选择内存序(
memory_order_relaxed
、acq_rel
等),最弱满足语义即可,减小屏障开销。避免
seq_cst
(默认)的全序,除非严苛要求。
无锁数据结构
单生产者/单消费者环形队列、Michael–Scott 无锁队列、无锁哈希表等。
结合 CAS(
compare_exchange_weak/strong
)实现。
示例:无锁单生产者单消费者环形缓冲
template<typename T, size_t N>
class SPSCQueue {
alignas(64) std::atomic<size_t> head{0}, tail{0};
T buffer[N];
public:
bool push(const T& v) {
size_t t = tail.load(std::memory_order_relaxed);
size_t h = head.load(std::memory_order_acquire);
if ((t + 1) % N == h) return false; // 满
buffer[t] = v;
tail.store((t + 1) % N, std::memory_order_release);
return true;
}
bool pop(T& v) {
size_t h = head.load(std::memory_order_relaxed);
size_t t = tail.load(std::memory_order_acquire);
if (h == t) return false; // 空
v = buffer[h];
head.store((h + 1) % N, std::memory_order_release);
return true;
}
};
注意:std::atomic
并非总比锁快——对于复杂操作(跨多个原子变量或依赖内存顺序的场景),仍需谨慎设计。
4. 锁域最小化与上下文切换
细化锁粒度
- 按数据分区上锁,避免全局锁;如分段锁(sharded lock)、细粒度读写锁。只在绝对必要的数据访问前后加锁,避免在锁内执行冗长计算或阻塞操作(I/O、系统调用)。
减少锁持有时间
- 将临界区内工作量降到最低:只做必需的状态修改,耗时操作(I/O、复杂计算)放到外部。
自旋 + 后退策略
- 在短锁等待场景下自旋(
spinlock
),避免线程切换开销;自旋若超时再std::this_thread::yield()
或挂起。在临界区非常短时,用自旋锁比阻塞锁(blocking mutex)更高效,因为线程不会进入内核调度:
- 在短锁等待场景下自旋(
std::atomic_flag lock = ATOMIC_FLAG_INIT;
void spin_lock() {
while (lock.test_and_set(std::memory_order_acquire)) {
// 轻微让出,避免总是占用 CPU
std::this_thread::yield();
}
}
void spin_unlock() {
lock.clear(std::memory_order_release);
}
减少线程数:总线程数超过 CPU 核数时,会带来频繁的线程切换;线程池应与硬件资源匹配。
批处理与队列:将大量小任务聚合成批次处理,减少每个任务的调度次数。
5. 缓存优化(Cache-Aware & Cache-Friendly)
数据对齐与避开伪共享
对齐:使用
alignas(64)
(假设缓存行 64 字节)对热点结构体或数组进行对齐,使其恰好占满一个或多个缓存行,避免跨行访问。填充(Padding):在并发写入的不同成员之间添加填充,保证不同线程操作的数据位于不同缓存行,消除伪共享。
内存布局(Data Layout)
结构体改造:将访问频率高的字段靠拢放在一起,或采用“结构体数组”(Array of Structures, AoS) vs “数组结构体”(Structure of Arrays, SoA),根据访问模式优化。
连续内存:优先使用
std::vector
或自己管理的连续内存,避免链表等指针追踪带来的缓存缺失(cache miss)。数据预取(Prefetch)
- 在访问大数组或循环中,可使用编译器内建函数(如 GCC 的
__builtin_prefetch
)手动提示数据到 L1/L2 缓存,以隐藏内存访问延迟。
- 在访问大数组或循环中,可使用编译器内建函数(如 GCC 的
示例:避免伪共享
struct alignas(64) Counter {
std::atomic<uint64_t> cnt;
char pad[64 - sizeof(std::atomic<uint64_t>)];
};
Counter counters[NUM_THREADS];
6. 分支预测优化
减少不可预测分支
- 将分支改写为查表(lookup table)或条件赋值(ternary operator)。
static const int lookup[256] = { /* 预先计算好的值 */ };
int foo(uint8_t x) { return lookup[x]; }
- 代码布局:将“常见”分支放在 if-else 的‘if’分支,以符合 CPU 的静态预测倾向;或使用
__builtin_expect
显式标注:
if (__builtin_expect(error_code != 0, 0)) {
// 少见的分支
}
if (unlikely(error)) { /* C++20特性 更不常走的路经*/ }
- 减少分支:在热循环中,尽量用算术或位操作替代条件跳转,或提前合并判断,将多重分支扁平化。
7. 系统调用与 I/O 优化
批量系统调用
- 批量 I/O:网络或文件读写时,合并多次小调用为一次大调用;使用异步 I/O 或零拷贝技术(如 Linux 的
sendfile
,mmap
;将多次写合并为一次writev
;Net I/O 用sendmmsg
/recvmmsg
)。
- 批量 I/O:网络或文件读写时,合并多次小调用为一次大调用;使用异步 I/O 或零拷贝技术(如 Linux 的
避免频繁获取时间:
gettimeofday
、std::chrono::system_clock::now()
等系统调用较慢,可用clock_gettime(CLOCK_MONOTONIC_RAW)
或用户态高速时钟库,必要时每隔一段周期更新一次缓存的时间戳。异步 / 事件驱动 I/O
- Linux 下用
io_uring
、epoll
;Windows 用 IOCP。
- Linux 下用
减少上下文切换
- Reserve 线程专门做 I/O,避免计算线程因 I/O 耽搁而切换;或用用户态线程(如
boost::fibers
)。
- Reserve 线程专门做 I/O,避免计算线程因 I/O 耽搁而切换;或用用户态线程(如
用户态队列:网络高性能库(如 DPDK、netmap)或用户态网络栈,绕过内核态上下文切换。