Cache管理-1-高速缓存伪共享避免
一、简介
基于msm-5.4
伪共享就是位于同一缓存行的不同数据被不同CPU频繁访问,导致高速缓存颠簸的问题。
解决办法就是让多线程操作的数据出在不同的高速缓存行,通常采用 高速缓存行填充技术 或 高速缓存行对齐技术,即让数据结构按高速缓存行对齐,并且尽可能填充满一个缓存行大小。
有些情况下高速缓存伪共享会严重影响性能,并且比较难以发现,需要在编程的时候特别小心数据结构里有没有可能出现不同CPU频繁访问某些成员的情况。
二、高速缓存行对齐技术
1. 说明
如下 counter_s 结构,让其起始地址按高速缓存行大小对齐,通过填充pad[4] 让整个结构都缓存到一个高速缓存行中。
typedef struct counter_s { XXX; uint64_t pad[4]; //凑够64B } counter_s_t __attribute__(__aligned__(SMP_CACHE_BYTES));
2. msm-5.4中相关实现
//linux/cache.h #define L1_CACHE_BYTES (1 << L1_CACHE_SHIFT) //(1<<6)=64B #define SMP_CACHE_BYTES L1_CACHE_BYTES //64B #define ____cacheline_aligned __attribute__((__aligned__(SMP_CACHE_BYTES))) #define ____cacheline_aligned_in_smp ____cacheline_aligned #define __cacheline_aligned __attribute__((__aligned__(SMP_CACHE_BYTES), __section__(".data..cacheline_aligned"))) #define __cacheline_aligned_in_smp __cacheline_aligned #define INTERNODE_CACHE_SHIFT L1_CACHE_SHIFT //6 #define ____cacheline_internodealigned_in_smp __attribute__((__aligned__(1 << (INTERNODE_CACHE_SHIFT)))) //也是高速缓存行大小对齐
____cacheline_aligned 宏用来让数据结构首地址按L1高速缓存对齐。用法如下:
//[1] 修饰结构体变量 static struct clock_data cd ____cacheline_aligned = { //sched_clock.c XXX; }; static struct caam_qi_priv qipriv ____cacheline_aligned; //qi.c //[2] 修饰结构体成员(一个结构体中可以有多个成员都使用, 参考 struct message_queue) struct workqueue_struct { //workqueue.c XXX; unsigned int flags ____cacheline_aligned; XXX; }; //[3] 修饰结构体定义 struct caam_qi_pcpu_priv { //qi.c XXX; } ____cacheline_aligned; //[4] 修饰数组变量 u32 crypto_ft_tab[4][256] ____cacheline_aligned = { XXX; };
即修饰变量或结构体都要放在后面,变量定义并初始化时,放在初始化数据前面。
3. 实验
#include <stdio.h> #include <stdint.h> #define user_cacheline_aligned __attribute__((aligned(64))) typedef struct counter_s { uint64_t a; uint64_t pad[4]; } counter_s_t user_cacheline_aligned; int main() { counter_s_t c1; printf("sizeof(counter_s_t)=%lu, &c1=%p\n", sizeof(counter_s_t), &c1); return 0; }
带上 user_cacheline_aligned 属性后,起始地址都是64字节对齐。此时结构体大小还是40字节(需要通过pad[]填充到)。
sizeof(counter_s_t)=40, &c1=0x7ffcb4bcd040
sizeof(counter_s_t)=40, &c1=0x7ffdcc55fb80
sizeof(counter_s_t)=40, &c1=0x7ffcb357f0c0
不带 user_cacheline_aligned 属性,起始地址非64B对齐。
sizeof(counter_s_t)=40, &c1=0x7ffe13e57c90
sizeof(counter_s_t)=40, &c1=0x7ffd4c969370
sizeof(counter_s_t)=40, &c1=0x7ffcfc870150
实测,即使结构体大小调整成64B, 若不带此对齐属性,其起始地址也不会64B对齐。
三、高速缓存行填充技术
1. 介绍
数据结构中频繁访问的成员可以独占一个高速缓存行,或者相关成员在高速缓存行中彼此错开,以提高访问效率。
例如,Linux内核中的zone结构使用填充字节的方式让频繁访问的成员在不同的缓存行中。下面示例代码中,initialized 和 free_area 会在高速缓存行中彼此错开。
struct zone { int initialized; ZONE_PADDING(_pad1_); // struct free_area free_area[MAX_ORDER]; };
ZONE_PADDING 定义如下:
struct zone_padding { char x[0]; } ____cacheline_internodealigned_in_smp; #define ZONE_PADDING(name) struct zone_padding name;
2. 实验
#include <stdio.h> #include <stdint.h> #define user_cacheline_internodealigned_in_smp __attribute__((aligned(64))) struct zone_padding { char x[0]; } user_cacheline_internodealigned_in_smp; #define ZONE_PADDING(name) struct zone_padding name; //让a和b的地址都cache line对齐 struct zone { uint64_t a; ZONE_PADDING(_pad1_); // uint64_t b; } user_cacheline_internodealigned_in_smp; int main() { struct zone z; printf("sizeof(struct zone)=%lu, &z.a=%p, &z.b=%p\n", sizeof(struct zone), &z.a, &z.b); return 0; }
可以看到 a 的地址和 b 的地址都是cache line对齐的。它会自动填充到cache line大小。
sizeof(struct zone)=128, &z.a=0x7ffdd5297980, &z.b=0x7ffdd52979c0 sizeof(struct zone)=128, &z.a=0x7ffed9d8d700, &z.b=0x7ffed9d8d740 sizeof(struct zone)=128, &z.a=0x7ffc06cfb4c0, &z.b=0x7ffc06cfb500
四、缓存伪共享介绍-chatGPT
1. 什么是伪共享
伪共享是多核系统中的一种性能问题:两个或多个 CPU 核心各自频繁访问逻辑上独立的变量,但这些变量恰好位于同一条 cache line 中,导致各核心的 cache 不断互相无效化(invalidate),产生大量缓存一致性流量,严重降低性能。
"伪"的含义:各核心之间并没有真正共享数据(各自操作不同变量),但硬件缓存一致性协议以 cache line 为粒度工作,误认为它们在"争用"同一份数据。
2. 硬件背景
CPU 0 CPU 1 CPU 2 CPU 3 ┌───────┐ ┌───────┐ ┌───────┐ ┌───────┐ │ L1 $ │ │ L1 $ │ │ L1 $ │ │ L1 $ │ │64B line│ │64B line│ │64B line│ │64B line│ └───┬───┘ └───┬───┘ └───┬───┘ └───┬───┘ │ │ │ │ └───────────────────┴───────────────────┴───────────────────┘ Interconnect / Bus (MESI 协议维护一致性) │ ┌──────┴──────┐ │ 主存 RAM │ └─────────────┘
关键约束:
(1) Cache line 是缓存操作的最小单位(ARM64/x86 通常是 64 字节);
(2) CPU 不能只 invalidate cache line 中的某几个字节,要无效就整条无效;
(3) MESI 协议:一个核心写某条 cache line 时,其他核心持有的同一 line 副本必须被标记为 Invalid;
3. 伪共享如何发生
/* 错误示例:两个计数器紧挨着 */ struct counters { unsigned long cpu0_count; //offset 0, 8 字节 unsigned long cpu1_count; //offset 8, 8 字节 }; //总共 16 字节 —— 假设两个变量在同一条 64B cache line 中 static struct counters g_cnt; /* CPU 0 频繁执行 */ void cpu0_work(void) { g_cnt.cpu0_count++; //只碰 cpu0_count } /* CPU 1 频繁执行 */ void cpu1_work(void) { g_cnt.cpu1_count++; //只碰 cpu1_count }
内存布局:
Cache line (64 bytes): ┌─────────────────────────────────────────────────────────────────┐ │ cpu0_count │ cpu1_count │ ... 剩余 48 字节 padding ... │ │ (8 bytes) │ (8 bytes) │ │ └─────────────────────────────────────────────────────────────────┘ ↑ CPU0 写这里 ↑ CPU1 写这里 虽然各写各的,但整条 cache line 在两个核心之间不断弹跳。
执行时序:
时间 T1: CPU0 写 cpu0_count → CPU0 的 cache line 变为 Modified → CPU1 的同一 cache line 被 invalidate(变为 Invalid)
时间 T2: CPU1 写 cpu1_count → CPU1 需要先从 CPU0(或内存)重新加载这条 cache line → CPU1 的 cache line 变为 Modified → CPU0 的 cache line 被 invalidate
时间 T3: CPU0 再次写 cpu0_count → CPU0 需要从 CPU1 重新加载... → 循环往复
每次写操作都要跨核传输整条 64 字节的 cache line,代价极大(几十到几百个 CPU 周期####)。
4. 性能影响有多大
-------------------------------------------------------------------- 操作 延迟(典型 ARM64) -------------------------------------------------------------------- L1 cache hit ~1-2 ns L2 cache hit ~5-10 ns 跨核 cache line transfer ~30-100 ns 主存访问 ~100-200 ns --------------------------------------------------------------------
伪共享把本来 ~2ns 的 L1 命中变成几十 ns 的跨核传输,如果涉及的核心更多,惩罚更大。
5. Linux 内核中的解决方案
5.1 ____cacheline_aligned / __cacheline_aligned
struct counters { unsigned long cpu0_count ____cacheline_aligned; //强制 64B 对齐 unsigned long cpu1_count ____cacheline_aligned; //独占一条 cache line };
内存布局变为:
Cache line 0 (64 bytes): ┌─────────────────────────────────────────────────────────────────┐ │ cpu0_count │ padding (56 bytes) │ └─────────────────────────────────────────────────────────────────┘ Cache line 1 (64 bytes): ┌─────────────────────────────────────────────────────────────────┐ │ cpu1_count │ padding (56 bytes) │ └─────────────────────────────────────────────────────────────────┘
两个变量各占独立的 cache line,互不干扰。代价:多浪费了 padding 空间。
5.2 Per-CPU 变量(最彻底的方案)
static DEFINE_PER_CPU(unsigned long, my_count); void cpu_work(void) { this_cpu_inc(my_count); //每个 CPU 操作自己的副本,天然不会伪共享#### } /* 需要总数时汇总 */ unsigned long total = 0; int cpu; for_each_possible_cpu(cpu) total += per_cpu(my_count, cpu);
Per-CPU 变量每个 CPU 有独立的内存区域,物理地址完全不同,不可能在同一条 cache line 中####。
5.3 __cacheline_aligned_in_smp
struct some_struct { /* 频繁读的数据 */ unsigned long read_mostly_field; /* 强制分隔到下一条 cache line */ unsigned long frequently_written __cacheline_aligned_in_smp; };
_in_smp 后缀表示只在 SMP 系统中对齐(UP 系统不需要,节省内存)。
5.4 结构体设计原则——按访问模式分组
/* 好的设计:按"谁写"分组 */ struct good_design { /* 组1:初始化后只读,所有 CPU 共享读取无开销 */ const unsigned long config; const unsigned long base_addr; /* 分隔线 */ ____cacheline_aligned_in_smp /* 组2:只有 owner CPU 写,其他 CPU 不碰 */ unsigned long local_counter; unsigned long local_state; ____cacheline_aligned_in_smp /* 组3:需要加锁的共享写入 */ spinlock_t lock; unsigned long shared_counter; };
6. 内核中的真实案例
6.1 struct zone(内存管理)
struct zone { /* 只读或很少修改的字段 */ unsigned long watermark[NR_WMARK]; long lowmem_reserve[MAX_NR_ZONES]; /* 这个 padding 就是为了防止伪共享 */ ZONE_PADDING(_pad1_) /* 频繁修改的字段(lock 和 free_area) */ spinlock_t lock; struct free_area free_area[MAX_ORDER]; ZONE_PADDING(_pad2_) /* 统计计数,per-cpu 更新 */ unsigned long vm_stat[NR_VM_ZONE_STAT_ITEMS]; };
ZONE_PADDING 宏插入足够大的 padding 确保不同访问频率的字段组在不同的 cache line 上。
6.2 Per-CPU pageset
struct per_cpu_pages { spinlock_t lock; int count; int high; int batch; struct list_head lists[NR_PCP_LISTS]; } ____cacheline_aligned_in_smp; //每个 CPU 的 PCP 结构独占 cache line
TODO: 每cpu变量的结构在定义时也要显式指定吗?
6.3 struct task_struct
struct task_struct { /* 调度器频繁访问的"热"字段放在前面 */ unsigned int flags; int on_cpu; int prio; ... /* 让不同子系统频繁修改的字段分开 */ /* ... 中间有大量字段 ... */ struct mm_struct *mm ____cacheline_aligned; };
7. 如何检测伪共享
7.1 perf c2c(最直接的工具)
# 记录 cache line 争用事件 perf c2c record -a -- sleep 5 # 分析报告 perf c2c report --stdio # 输出中会显示哪些地址有跨核 cache line 竞争 # Shared Data Cache Line Table # Address Pid Symbol Rmt LLC Lcl LLC # 0xffff1234... 1234 g_cnt+0x8 (cpu1_count) 85% 15%
7.2 PMU 计数器
# ARM64 上 perf stat -e bus_access,l2d_cache_refill,remote_access ./benchmark # 如果 remote_access / bus_access 比例异常高,说明存在跨核传输。
8. 总结
------------------------------------------------------------------ 概念 说明 ------------------------------------------------------------------ 根因 Cache 一致性协议以 cache line(64B)为单位操作 表现 逻辑无关的变量因物理相邻而导致跨核 cache 弹跳 检测 perf c2c、PMU remote access 计数器 解决 ____cacheline_aligned、Per-CPU 变量、按访问模式分组结构体 代价 空间换时间(padding 浪费内存) ------------------------------------------------------------------
posted on 2025-03-03 10:27 Hello-World3 阅读(1) 评论(0) 收藏 举报
浙公网安备 33010602011771号