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)    收藏  举报

导航