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

并发读取场景下本地修改缓存行读取性能下降的原因探究

问题:写线程读取本地修改Cache Line的性能瓶颈分析

实验背景与预期

多个线程并行访问同一Cache Line:其中一个写线程反复对该Cache Line执行读写操作,其余读线程仅反复读取该Cache Line。基于MESI协议的失效机制,写线程写入前会让读线程的本地缓存失效,因此读线程性能下降是预期内的,但写线程读取自己修改过的本地Cache Line应该非常快——毕竟本地写入不需要触发缓存失效操作。

实际异常现象

在搭载两颗Intel Xeon Scalable Gold 5220R处理器(每颗24核,主频2.20GHz)的双路服务器上测试时,出现了异常:写线程对该Cache Line的读取操作反而成为了性能瓶颈。

测试代码

使用gcc 8.4.0编译,开启-O2优化:

#define _GNU_SOURCE

#include <stdio.h>
#include <pthread.h>
#include <sched.h>
#include <unistd.h>
#include <sys/syscall.h>

#define CACHELINE_SIZE  64

volatile struct {
    /* cacheline 1 */
    size_t x, y;
    char padding[CACHELINE_SIZE - 2 * sizeof(size_t)];
    /* cacheline 2 */
    size_t p, q;
} __attribute__((aligned(CACHELINE_SIZE))) data;

static inline void bind_core(int core) {
    cpu_set_t mask;
    CPU_ZERO(&mask);
    CPU_SET(core, &mask);
    if ((sched_setaffinity(0, sizeof(cpu_set_t), &mask)) != 0) {
        perror("bind core failed\n");
    }
}

#define smp_mb()    asm volatile("lock; addl $0,-4(%%rsp)" ::: "memory", "cc")

void *writer_work(void *arg) {
    long id = (long) arg;
    int i;
    bind_core(id);
    printf("writer tid: %ld\n", syscall(SYS_gettid));
    while (1) {
        /* read after write */
        data.x = 1;
        data.y;
        for (i = 0; i < 50; i++) __asm__("nop"); // to highlight bottleneck
    }
}

void *reader_work(void *arg) {
    long id = (long) arg;
    bind_core(id);
    while (1) {
        /* read */
        data.y;
    }
}

#define NR_THREAD   48

int main() {
    pthread_t threads[NR_THREAD];
    int i;
    printf("%p %p\n", &data.x, &data.p);
    data.x = data.y = data.p = data.q = 0;
    pthread_create(&threads[0], NULL, writer_work, 0);
    for (i = 1; i < NR_THREAD; i++) {
        pthread_create(&threads[i], NULL, reader_work, i);
    }
    for (i = 0; i < NR_THREAD; i++) {
        pthread_join(threads[i], NULL);
    }
    return 0;
}

Perf性能分析结果

使用perf record -t <tid>收集写线程的cycles事件,通过perf annotate writer_work查看指令耗时占比:

第一次测试(nop循环在加载后)

:          while (1) {
         :              /* read after write */
         :              data.x = 1;
    0.20 :        a50:   movq   $0x1,0x200625(%rip)        # 201080 <data>
         :              data.y;
   94.40 :        a5b:   mov    0x200626(%rip),%rax        # 201088 <data+0x8>
    0.03 :        a62:   mov    $0x32,%eax
    0.00 :        a67:   nopw   0x0(%rax,%rax,1)
         :              for (i = 0; i < 50; i++) __asm__("nop");
    0.03 :        a70:   nop
    0.03 :        a71:   sub    $0x1,%eax
    5.17 :        a74:   jne    a70 <writer_work+0x50>
    0.15 :        a76:   jmp    a50 <writer_work+0x30>

可见data.y的加载指令占用了94.40%的cycles,是明显瓶颈。

调整nop循环到加载指令前

为排除“存储指令后的任意指令被误判为瓶颈”的可能,将nop循环移到加载指令前,perf结果仍显示加载指令为瓶颈:

:      writer_work():
         :          while (1) {
         :              /* read after write */
         :              data.x = 1;
    0.00 :        a50:   movq   $0x1,0x200625(%rip)        # 201080 <data>
    0.03 :        a5b:   mov    $0x32,%eax
         :              for (i = 0; i < 50; i++) __asm__("nop");
    0.03 :        a60:   nop
    0.09 :        a61:   sub    $0x1,%eax
    6.24 :        a64:   jne    a60 <writer_work+0x40>
         :              data.y;
   93.60 :        a66:   mov    0x20061b(%rip),%rax        # 201088 <data+0x8>
         :              data.x = 1;
    0.02 :        a6d:   jmp    a50 <writer_work+0x30>

已排除的可能性

  • 注释写线程的存储操作后,写线程的读取不再是瓶颈,说明性能下降与Cache Line的修改操作相关;
  • 注释读线程的读取操作后,写线程的读取也不会被标记为瓶颈,说明问题与并发读线程的存在有关;
  • 将写线程读取的data.y改为另一Cache Line的变量后,耗时显著降低,确认问题局限于同一Cache Line的并发访问;
  • 调整nop循环位置后,加载指令仍为瓶颈,排除了“存储指令后的指令被perf误判”的可能性。

核心疑问

当本地修改的Cache Line被并发读取时,写线程读取该Cache Line的性能下降由何导致?

内容的提问来源于stack exchange,提问作者hhusjr

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.24 18:06:29