多线程读写场景中Read-Modify-Store操作为何出现两次L1D缓存缺失?
问题背景
搭建了一个多线程环境:一个写线程对共享std::atomic变量执行Read-Modify-Store操作,另一个读线程持续读取该变量以检查值是否更新。
代码示例
写线程
void writer(std::atomic<size_t>& shared_value, size_t total_writes) { bindThreadToCore(0); // 绑定写线程到核心0 // PerfMeasure pm(0x151); // 测量L1D缓存缺失 size_t writer_local_value = 0; for (size_t i = 0; i < total_writes; ++i) { // pm.enable(); for(int j = 1; j < 100; ++j) { writer_local_value = shared_value.load(std::memory_order_acquire); if(shared_value.compare_exchange_strong(writer_local_value, writer_local_value+1, std::memory_order_relaxed)) { break; } } // pm.disable(); writer_local_value++; // 模拟工作负载 volatile int waste = 0; for (int j = 0; j < 10000; ++j) { ++waste; // 阻止编译器优化 } } }
读线程
void reader() { size_t last_seen_value = 0; while (true) { size_t current_value = shared_value.load(); // 读取变量 if (current_value != last_seen_value) { last_seen_value = current_value; // 值更新时执行操作 } } }
环境配置
- 平台:x86_64
- 操作系统:Ubuntu
- CPU:12th Gen Intel(R) Core(TM) i9-12900K
- 缓存缺失测量工具:perf_event_open
- 测量事件:L1D.REPLACEMENT(原始计数器代码0x151,对应L1D缓存缺失)
观测结果
- 当写线程执行Read-Modify-Store操作时,
shared_value出现两次L1D缓存缺失:一次在shared_value.load()阶段,一次在shared_value.store()阶段。 - 若替换为无前置读取的简单
store操作(如下代码),仅出现一次L1D缓存缺失:
void writer_simple() { while (true) { shared_value.store(42); // 仅执行写入 std::this_thread::sleep_for(std::chrono::milliseconds(1)); } }
核心问题
为什么Read-Modify-Store操作会导致两次L1D缓存缺失?按MESI协议,初始读取已获取缓存行,后续写入不应再产生缺失,是什么导致了这种差异?
补充说明
- 尝试过为
std::atomic变量使用不同内存一致性模型,缓存缺失数量无变化。 - 已确保
shared_value对齐到缓存行,线程间无伪共享。
解答
1. 读线程持续轮询导致缓存行所有权流转
读线程在高频轮询shared_value,会不断抢占缓存行的所有权:
- 写线程通过
load获取缓存行的Shared(S)状态,此时读线程也能持有S状态。 - 但读线程的持续
load请求会不断向缓存控制器发送信号,可能让缓存行从写线程的L1D缓存中被逐出,或状态回到其他核心的S状态。 - 当写线程准备执行
compare_exchange_strong的写入操作时,需要将缓存行升级为Exclusive(E)/Modified(M)状态,若此时缓存行不在本地L1D,就会触发第二次缓存缺失。
2. 非原子化的读-改-写拆分导致状态窗口
你代码中的“读+条件写入”不是原子的Read-Modify-Store操作:
- 单独的
load指令获取缓存行S状态后,到compare_exchange_strong执行前存在时间窗口,读线程的持续访问可能已经改变了缓存行的状态。 compare_exchange_strong执行时需要先确保缓存行处于独占状态,若此时缓存行已不在写线程L1D中,就会触发第二次缺失。
而单独的store操作行为不同:
- x86平台下的
store会直接尝试获取缓存行的E/M状态,若缓存行已在本地L1D(即使是S状态),可直接升级状态无需重新加载;仅当缓存行完全不在本地缓存时,才会触发一次缺失。 - 加上
sleep_for让读线程有足够时间获取缓存行,但写入时仅需一次所有权获取,因此仅出现一次缺失。
3. 12代酷睿混合架构的额外影响
12代酷睿(Alder Lake)是混合架构,包含性能核(P核)和能效核(E核),跨核心的缓存同步开销更大:
- 若写线程绑定在P核、读线程运行在E核,缓存行更容易在不同核心间流转,写线程需要多次重新获取缓存行所有权,进一步加剧了两次缺失的情况。
验证与优化建议
- 暂停读线程,观察写线程缓存缺失是否降为一次,验证读线程轮询是关键因素。
- 将
load和compare_exchange_strong合并为真正的原子Read-Modify-Store操作(如fetch_add),减少状态窗口:
编译出的shared_value.fetch_add(1, std::memory_order_relaxed);lock xadd指令会原子完成读-改-写,缓存行状态流转更紧凑,大概率只会触发一次缓存缺失。
内容的提问来源于stack exchange,提问作者user26135499
相关产品推荐
相关产品推荐

