You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

多线程读写场景中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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.15 18:08:11