欢迎光临
我们一直在努力

多线程性能杀手:你以为是 atomic,其实是 cache line 在打架

很多人第一次写高并发代码时,会写出类似这样的代码:

std::atomic<long long> counter;
counter.fetch_add(1);

直觉是:

  • 没加锁
  • 原子操作
  • 应该可以很好扩展

但实际运行结果往往是:

线程数增加 → 性能不升反降
CPU 很高 → 吞吐没提升

这类问题非常常见,而且经常被误解为:

“atomic 很慢”

但这个结论并不准确。


一、问题的本质

真正的问题不是 atomic,而是:

多个线程在频繁修改同一块 cache line

这会触发底层硬件机制:

  • 缓存一致性协议
  • cache line 在 CPU 核之间来回迁移
  • 最终导致隐式串行化

一句话总结:

共享写热点(write contention)会让多核退化为单核执行


二、什么是 Cache Line(关键基础)

理解这个问题,必须先理解 cache line。


1. 定义

Cache Line 是 CPU 在缓存与内存之间传输数据的最小单位

在绝大多数现代 CPU 上:

Cache Line = 64 字节


2. 为什么存在

CPU 和内存的速度差距很大:

层级延迟
L1 Cache ~1 ns
L2 Cache ~3–5 ns
L3 Cache ~10–20 ns
内存 ~100 ns

如果每次只读取 8 字节,效率很低。

因此 CPU 采用:

一次读取一整块(64B)


3. 实际访问行

long long x = arr[0];

你以为:

读了 8 字节

实际上:

读了 arr[0] 所在的整个 cache line(64 字节)


4. 关键结论

CPU 操作的不是变量,而是 cache line


三、cache line 如何导致性能问题

1. 多线程写同一个变量

std::atomic<long long> counter;
counter.fetch_add(1);

多个线程执行:

修改同一个 cache line


2. CPU 内部发生了什么

为了保证一致性,CPU 使用类似 MESI 的协议。

写操作必须:

获取 cache line 的独占权限(Modified)

多个核心同时写时:

Core A 修改 → Core B 使 A 失效 → 抢占
Core B 修改 → Core C 抢占

结果:

cache line 在多个核心之间反复迁移


3. 最终效果

同一时刻只能有一个核心修改数据

也就是说:

代码看起来是并发的,硬件层面却是串行的


四、一个直观实验

1. atomic 版本

std::atomic<long long> counter{0};

void work() {
for (int i = 0; i < 1e8; ++i) {
counter.fetch_add(1, std::memory_order_relaxed);
}
}


2. thread-local 版本

thread_local long long local = 0;

void work() {
for (int i = 0; i < 1e8; ++i) {
local++;
}
}


3. 典型结果

线程数atomicthread-local
1
4 一般 接近线性提升
8 变慢 继续提升

4. 结论

问题不在 atomic,而在“多个线程写同一个 cache line”


五、这是一整类问题(不是个例)

这类问题在工程中非常常见,可以统一抽象为:

多线程 + 高频写 + 同一 cache line

下面是常见场景。


1. 全局计数器

global_qps++;
global_bytes += len;


2. 自旋锁

while (lock.test_and_set()) {}

所有线程不断访问同一个变量。


3. 无锁队列

tail.fetch_add(1);

head/tail 成为热点。


4. shared_ptr 引用计数

refcnt.fetch_add(1);

热点对象会放大问题。


5. 日志系统

write_pos.fetch_add(…)

高频写入导致竞争。


6. 内存分配器

早期:

全局 free list

后来:

thread cache(tcmalloc / jemalloc)


7. False Sharing(最隐蔽

struct {
long long a;
long long b;
};

两个线程

a++;
b++;

问题:

a 和 b 在同一个 cache line

结果仍然冲突。


六、如何识别这类问题

1. 扩展性异常

线程增加 → 性能不升反降


2. CPU 利用率异常

CPU 很高,但吞吐没增长


3. perf 工具

perf c2c record ./app
perf c2c report

可以看到:

  • cache line bouncing
  • HITM(hit modified)

七、解决方案(核心原则)

避免共享写,改为分散写 + 延迟汇总


1. thread-local(最推荐)

thread_local long long local;


2. 分片(sharding)

counter[thread_id % N]++;


3. padding / 对齐

struct alignas(64) Counter {
long long val;
};


4. 批量更新

local++;
if (local > 1024) {
global += local;
local = 0;
}


5. 分层聚合

线程 → worker → 请求(thread)→ 全局


八、几个重要认知纠正

1. atomic 不等于慢

正确说法:

atomic + 共享写 = 慢


2. 变量小不代表安全

bool flag;

仍然占用一个 cache line。


3. 无锁不等于高性能

无锁 ≠ 无竞争


九、总结

多核系统中,性能瓶颈往往不是计算,而是“多个核心争抢同一块 cache line”;一旦发生共享写热点,系统会被硬件强制串行化。

高性能并发设计的核心,不是“避免加锁”,而是“避免共享写”。

赞(0)
未经允许不得转载:171主机测评 » 多线程性能杀手:你以为是 atomic,其实是 cache line 在打架
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址