并发读取场景下本地修改缓存行读取性能下降的原因探究
问题:写线程读取本地修改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
相关产品推荐
相关产品推荐

