时间轴

2025-10-26

init

参考文档:

为什么要cache一致性(cache coherency)?

  • 系统中各级cache都有不同的数据备份,例如每个CPU核心都有L1 cache

系统中各级cache都有不同的数据备份

  • cache一致性关注的是同一个数据在多个高速缓存和内存中的一致性问题,解决高速缓存一致性的方法主要是总线监听协议,例如MESI协议
  • 需要关注cache一致性的例子:
    • 驱动中使用DMA(数据cache和内存不一致)
    • Self-modifying code(数据cache的数据可能比指令cache新)
    • 修改了页表(TLB里保存的数据可能过时)

ARMcache一致性的演进

ARM cache一致性演进

  • Cortex-A8是单核架构,没有核心之间的cache一致性问题,但是存在DMA和cache之间的一致性问题
  • Cortex-A9的多核版本(MPCore)存在核心之间的cache一致性问题,通常的做法是在硬件上实现一个MESI协议
  • Cortex-A15出现了大小核架构(big.LITTLE),比如一个cluster全部是大核另一个全部是小核,因此cluster和cluster之间也需要cache的一致性,需要AMBA Coherency Extension来处理,在ARM中有现成的IP(在 IC 设计 里,IP 核 = 已经设计好、验证过的电路模块,可以作为“积木”直接拿来复用)可以使用,比如CCI-400CCI-500
  • 单核处理器(Cortex-A8)
    • 单核,没有cache一致性问题
    • Cache管理指令仅仅作用于单核
  • 多核处理器(Cortex-A9 MP以及之后的处理器)
    • 硬件上支持cache一致性
    • Cache管理指令会广播到其他CPU核心

多核处理器的cache一致性

Cortex-A72 processor

系统级别的cache一致性

  • 系统cache一致性需要cache一致性内部总线(cache coherent interconnect)
    • AMBA 4协议有ACE(AXI Coherency Extensions)
    • AMBA 5协议有CHI

系统级别的cache一致性

Cache一致性的解决方案

  1. 关闭cache

    • 优点:简单
    • 缺点:性能低下,功耗增加
  2. 软件维护cache一致性

    • 优点:硬件RTL实现简单
    • 缺点:
      • 软件复杂度增加。软件需要手动clean/flush cache或者invalidate cache
      • 增加调试难度
      • 降低性能和增加功耗
  3. 硬件维护cache一致性

    MESI协议来维护多核cache一致性ACE接口来实现系统级别的cache一致性

    • 优点:对软件透明
    • 缺点:增加了硬件RTL实现难度和复杂度

多核之间的Cache一致性

  • 多核CPU产生cache一致性的原因:同一个内存数据在多个CPU核心的L1 cache中存在多个不同的副本,导致数据不一致

  • 维护cache一致性的关键是跟踪每一个cache line的状态,并根据处理器的读写操作和总线上的相应传输来更新cache line在不同CPU内核上的cache的状态,从而维护cache一致性

Cache一致性协议

  • 监听协议 (snooping protocol),每个高速缓存都要被监听或者监听其他高速缓存的总线活动

  • 目录协议 (directory protocol),全局统一管理高速缓存状态

  • MESI协议:
    • 1983年,James Goodman提出Write-Once总线监听协议,后来演变成目前最流行的MESI协议
    • 所有总线传输事务对于系统所有的其他单元是可见的,因为总线是一个基于广播通信的介质,因而可以由每个处理器的高速缓存来进行监听

总线监听和广播

Snoop control unit单元实现总线监听和广播

每个CPU的L1 cache也实现了总线监听功能

MESI协议

  • 每个cache line有四个状态
    • 修改(Modified)
    • 独占(Exclusive)
    • 共享(Shared)
    • 失效(Invalid)

MESI协议的四个状态

  • 修改M和独占状态E的cache line,数据都是独有的,不同点在于修改状态的数据是脏的,和内存不一致,而独占态的数据是干净的和内存一致。脏的cache line会被回写到内存,其后的状态变成共享态。
  • 共享状态S的cache line,数据和其他cache共享,只有干净的数据才能被多个cache共享
  • I状态表示这个cache line无效

MESI的操作

MESI的操作

MESI状态图

MESI状态图

MESI主要解决的是每个CPU中的local cache之间的一致性问题

每个CPU的本地高速缓存行的cache一致性问题

MESI M状态说明

MESI E状态和S状态说明

MESI I状态说明

MESI协议分析的一个例子

  • 假设系统中有4个CPU,每个CPU都有各自一级缓存,它们都想访问相同地址的数据A,大小为64字节。
    • T0时刻:4个CPU的L1 cache都没有缓存数据A,cache line的状态为I(无效的)
    • T1时刻:CPU0率先发起访问数据A的操作
    • T2时刻:CPU1也发起读数据操作
    • T3时刻:CPU2的程序想修改数据A中的数据
  • 请分析上述过程中,MESI状态的变化

T0时刻 4个CPU的L1 cache都没有缓存数据A,cache line的状态为I

T0时刻

T1时刻 CPU0率先发起访问数据A的操作

T1时刻

T2时刻 CPU1也发起读数据操作

T2时刻

T3时刻 CPU2的程序想修改数据A中的数据

T3时刻

高速缓存伪共享(False Sharing)

  • 如果多个处理器同时访问一个缓存行中不同的数据时,带来了性能上的问题
  • 举个例子:假设CPU0上的线程0想访问和更新struct data数据结构中的x成员,同理CPU1上的线程1想访问和更新struct data数据结构中的y成员,其中x和y成员都被缓存到同一个缓存行里。

Fasle Sharing的一个例子

分析:

T0时刻

T1时刻

T2时刻

T4时刻

T5时刻

之后会不停地在T4和T5之间重复,争夺cache line,不断地让对方的cache line无效,触发高速缓存写回内存。

解决办法

  • 高速缓存伪共享的解决办法就是让多线程操作的数据处在不同的告诉缓存行,通常可以采用高速缓存行填充(padding)技术或者高速缓存行对齐(align)技术,即让数据结构按照高速缓存行对齐,并且尽可能填充满一个高速缓存行大小。
  • 下面一个代码定义一个counter_s数据结构,它的起始地址按照高速缓存行的大小对齐,数据结构的成员通过pad[4]来填充。这样,counter_s的大小正好是一个cache line的大小,64个字节,而且它的起始地址也是cache line对齐

pad

尽可能让counter_s独占一个cache line而不和其他数据结构共享一个cache line

系统间的Cache一致性

系统间Cache一致性

CoreLink CCI-400

ARM面向服务器市场的CCI是CoreLink CCN

CoreLink Cache Coherent Network Family

CCN-512

读数据的例子

T0时刻

T1时刻

T2时刻

T3时刻

T3时刻另一种情况

写数据的例子

T0时刻

T1时刻

T2时刻

T3时刻

T4时刻

Cache一致性的案例

案例1:高速缓存伪共享的避免

  • 一些常用的数据结构在定义时就约定数据结构以一级缓存对齐。例如使用如下的宏来让数据结构首地址以L1 cache对齐
1
#define cacheline_aligned __attribute__((__aligned__(L1_CACHE_BYTES)))
  • 数据结构频繁访问的成员可以单独占用一个高速缓存行,或者相关的成员在高速缓存行中彼此错开,以提高访问效率。例如struct zone数据结构使用ZONE_PADDING技术(填充字节的方式)来让频繁访问的成员在不同的cache line中

    数据结构频繁访问的成员可以单独占用一个高速缓存行

案例2:DMA的cache一致性

DMA (Direct Memory Access)直接内存访问,它在传输过程中时不需要CPU干预的,可以直接从内存中读写数据

  • DMA产生cache一致性问题的原因:
    • DMA直接操作系统总线来读写内存,而CPU并不感知
    • 如果DMA修改的内存地址,在CPU的cache中有缓存,那么CPU并不知道内存数据被修改了,CPU依然去访问cache的旧数据,导致Cache一致性问题

DMA的Cache一致性问题

DMA的cache一致性解决方案

  • 硬件解决办法,需要ACE总线支持(咨询SoC vendor)

  • 使用non-cacheable的内存来进行DMA传输

    • 缺点:在不使用DMA的时候,CPU访问这个buffer会导致性能下降
  • 软件干预cache一致性,根据DMA传输数据的方向,软件来维护cache一致性

    • Case 1: 内存->设备FIFO (设备例如网卡,通过DMA读取内存数据到设备FIFO)

      • 在DMA传输之前,CPU的cache可能缓存了内存数据,需要调用cache clean/flush操作,把cache内容写入到内存中,因为CPU cache里可能缓存了最新的数据。

      CPU的Cache里有最新的数据但是DMA从内存中拿到的是旧数据

    • Case 2: 设备FIFO->内存(设备把数据写入到内存中)

      • 在DMA传输之前,需要把cache做invalid操作。因为此时最新数据在设备FIFO中,CPU缓存的cache数据是过时的,一会要写入新数据所以做invalid操作

      CPU的cache可能还有无用的数据

案例3:self-modifying code

  • 指令cache和数据cache是分开的。指令cache一般只读

  • 指令cache和数据cache的一致性问题。指令通常不能修改,但是在某些特殊情况下指令存在被修改的情况

  • self-modifying code,在执行过程中修改自己的指令(防止软件破解,或者gdb调试代码动态修改程序),过程如下

    • 要把修改的指令,加载到数据cache里
    • 程序(CPU)修改新指令,数据cache里缓存了最新指令

    存在问题:

    • 指令cache依然缓存了旧的指令,新指令还在数据cache里

    Self modifying code

解决思路

  • 使用cache clean操作,把cache line的数据写回到内存
  • 使用DSB指令保证其他观察者看到clean操作已经完成
  • 无效指令cache
  • 使用DSB指令确保其他观察者看到无效操作已经完成
  • ISB指令让程序重新预取指令

ARMv8.6手册B2.4.4章

Cache实验二:false sharing

实验2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
#include <pthread.h>
#include <stdio.h>
#include <sys/time.h>
#include <time.h>

struct data_with_false_sharing {

unsigned long x;
unsigned long y;

} __attribute__((__align__(64)));

struct padding {

char x[0]
} __attribute__((__align__(64)));

struct data_without_false_sharing {

unsigned long x;
struct padding _pad;
unsigned long y;
} __attribute__((__align__(64)));

#define MAX_LOOP 10000000000
void *access_data(void *param) {
unsigned long *data = (unsigned long *)param;
unsigned long i;
for (i = 0; i < MAX_LOOP; i++) {
*data = i;
}
}

int main(void) {
struct data_with_false_sharing data_wfs = {1, 2};
struct data_without_false_sharing data_wofs = {.x = 1, .y = 2};
pthread_t thread_1;
pthread_t thread_2;
unsigned long total_time;

struct timespec time_start, time_end;
clock_gettime(CLOCK_REALTIME, &time_start);
pthread_create(&thread_1, NULL, &access_data, (void *)&data_wfs.x);
pthread_create(&thread_2, NULL, &access_data, (void *)&data_wfs.y);
pthread_join(thread_1, NULL);
pthread_join(thread_2, NULL);
clock_gettime(CLOCK_REALTIME, &time_end);
total_time = (time_end.tv_sec - time_start.tv_sec) * 1000 +
(time_end.tv_nsec - time_start.tv_nsec) / 1000000;
printf("cache with false sharing: %lu ms \n", total_time);

clock_gettime(CLOCK_REALTIME, &time_start);
pthread_create(&thread_1, NULL, &access_data, (void *)&data_wofs.x);
pthread_create(&thread_2, NULL, &access_data, (void *)&data_wofs.y);
pthread_join(thread_1, NULL);
pthread_join(thread_2, NULL);
clock_gettime(CLOCK_REALTIME, &time_end);
total_time = (time_end.tv_sec - time_start.tv_sec) * 1000 +
(time_end.tv_nsec - time_start.tv_nsec) / 1000000;
printf("cache without false sharing: %lu ms \n", total_time);
}

实验2结果(qemu)

Cache实验三:flush cache实验

实验3

结果:

实验3结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
.global get_cache_line_size
get_cache_line_size:
mrs x0, ctr_el0
ubfm x0, x0, #16, #19
mov x1, #4
lsl x0, x1, x0
ret

/*
flush_cache_range(start, end)
*/
.global flush_cache_range
flush_cache_range:
stp x29, x30, [sp, -16]!
// start
mov x8, x0
// end
mov x9, x1

bl get_cache_line_size
// x3 = cache_line_size -1
sub x3, x0, #1
// bit clear, x4 = x8 & (~x3)
// 把 start 地址向下对齐到 cache line 边界
bic x4, x8, x3
// 循环清理cacheline
1:
dc civac, x4
add x4, x4, x0
cmp x4, x9
b.lo 1b

dsb ish

ldp x29, x30, [sp], 16

ret

armv8芯片手册Cache相关章节

  • ARM Architecture Reference Manual Armv8, for Armv8-A architecture profile

    • B2.4 Caches and memory hierarchy
    • D4.4 Cache Support

    • D5.11 Caches in a VMSAv8-64 implementation

  • ARM Cortex-A Series Programmer’s Guide for ARMv8-A

    • Chapter 11 Cache
  • ARM Cortex-A72 MPCore Processor Technical Reference Manual
    • 6: Level 1 Memory System
    • 7:Level 2 Memory System