当前位置: 首页 > news >正文

Cache的基本原理和缓存一致性

接触过很多软件工程师,在他们认识里,好像Cache的“透明”的,是硬件工程师的事情,并不怎么关心Cache的行为。但实际上想要进一步提升软件性能、优化系统设计,写出高效的代码,对Cache的理解使用是必不可少。

当然我也是在多次使用并调试过Cache后,才有一点点认识,把之前整理的一些笔记简单分享下,希望能够给需要的人提供一点帮助,另外如果问题之处,还请批评指正。

1 基础知识

1.1 什么是cache

**高速缓存(Cache)**是位于CPU和主存之间的告诉存储单元,专门用于缓存最近使用的数据。

1.2 为什么需要缓存 (Cache)​​

计算机存储系统存在一个基本矛盾:上层越是靠近CPU的存储设备容量越小、速度越快、价格也越贵,而下层越是远离CPU的存储设备容量越大、速度越慢、价格也越便宜。
显然,​​高性能与大容量难以在单一存储层同时实现​​。
在这里插入图片描述
为了弥补 CPU 处理速度与主内存(RAM)访问速度之间的巨大差距,缓存 (Cache) 技术应运而生。CPU 缓存通常分为多级,最常见的是 ​​L1(一级缓存)​​、​​L2(二级缓存)​​ 和 ​​L3(三级缓存)​​。下图是现在CPU架构中常见的大小核心设计,一般会设计三级Cache。
在这里插入图片描述

  1. ​​L1 Cache:​​
  • 最靠近 CPU 核心,速度最快(必须接近 CPU 的时钟频率),成本最高,容量最小(通常在几十 KB 级别)。
  • 一般分为​​指令缓存 (I-Cache)​​ 和​​数据缓存 (D-Cache)​​。两者原理类似,但 D-Cache 需要支持读写操作,而 I-Cache 通常只读,因此 D-Cache 的设计更为复杂。
  • 使用高速的多端口 SRAM 实现。大容量 SRAM 的查找时间会更长,这限制了 L1 的容量。
  1. ​​L2 Cache:​​
  • 一般在一个cluster内部共享,用来加速协调SMP多核的访问
  • 速度稍慢于 L1,成本和容量介于 L1 和 L3 之间(通常是几百 KB 到几 MB 级别)。
  • 通常是​​指令和数据共享​​的设计。它作为 L1 Cache 和更低层级存储(L3 或内存)之间的缓冲,旨在保存更多近期可能被访问的数据。
  1. ​​L3 Cache:​​
  • 位于 L2 Cache 和主内存之间,也可以称为system cache,通常被设计为​​所有核心共享​​的资源,减少访问主内存的次数
  • 速度比 L2 Cache ​​更慢​​,但​​远快于主内存​​。
  • 容量​​更大​​(通常是几 MB 到几十 MB 级别),单位容量成本相对 L2 更低。

当然上面的解释是显而易见的,下面是更加专业理论的解释为什么需要Cache,如下:

  1. 加速访问速度
    由于CPU的访问速度远高于主内存,频繁的数据交换会导致瓶颈。Cache通过缓存经常访问的数据减少对内存的依赖,显著加快数据访问速度。
  2. 利用局部原理
    基于时间和空间相关的特性,程序往往重复访问相同或者邻近的数据。Cache能够高效的捕捉这种模式,提高更快的数据响应

NOTE:
时间相关性:如果一个数据现在被访问了,那么在以后很有可能还会被访问
空间相关性:如果一个数据现在被访问了,那么它周围的数据在以后也很有可能被访问。

2 Cache的原理

当然网上有很多硬件大佬对Cache机制原理的分享,我这里算是分享下自己的学习,意在简单理解硬件行为的同时更好的提升软件质量。

Cache主要由两部分组成,Tag RAMData RAM

2.1 高速缓存(Cache)的核心组成​​

高速缓存的物理结构主要由两部分构成:

  1. Tag RAM​​
    ​​作用​​:存储缓存行(Cache Line)对应的​​内存地址高位信息​​(Tag)。
    ​​工作机制​​:通过比较访问地址的 Tag 部分与存储的 Tag,判断目标数据是否命中缓存。
    ​​
  2. Data RAM​​
    ​​作用​​:存储实际的内存数据块(Cache Data Block)。
    ​​设计依据​​:基于​​程序局部性原理​​(时间局部性 + 空间局部性),一次加载连续地址的数据。

另外一个Tag和它对应的所有数据组成的一行称为一个cacheline,是高速缓存操作的最小单位,一般设计在4-128bytes(我是用过的Cache line是32和64bytes的CPU,另外这里说的cacheline大小指的是data block的大小,不包含tag)。
在这里插入图片描述
Cache匹配简单流程(以读内存为例)

  • 1、地址拆分​​:访问地址 → [tag] + [index] + [offset]。
  • 2、定位Cache line​:用index找到对应的 Cache line。
  • 3、定位Cache line filed: 用offset找到在Cache line对应的位置
  • 4、tag匹配​​:比较所有Cache line 的Tag与地址的 Tag:
    • 命中(hit)​​:直接读取数据块,使用offset定位具体字节。
    • 未命中​(miss)​:从内存加载数据块,替换对应的Cache line(按替换策略)。

2.2 Cache的组成方式

2.2.1 直接映射缓存

Cache 被分成很多行(cacheline),一行可以存储主存的任意一个数据块,数据块大小等于cacheline;每个内存块只能放在Cache的一个固定位置。

举例看下,直接映射缓存的,假如当前使用Cache size是256bytes,8个cacheline,每个cacheline size是32bytes。此时CPU(x32)要访问0x73c这个地址。

  1. tag ram分布
  • offset:
    cacheline size是32bytes,所以要访问全部byte需要5bit(2^5=32)
  • index
    一共有8个cacheline,所以索引需要3bit(2^3=8)
  • tag
    当前是的地址位宽(AW)是32bit,则tag为24bit(32-5-3=24)

举例:地址0x73c解析为
在这里插入图片描述

  1. 定位流程,如图
    在这里插入图片描述
  • 1、 取index定位到对应的cacheline
  • 2、 取offset定位到对应的byte
  • 3、 取tag ram里的tag和地址中的tag比较,如果相同表明该是cache命中(hit),否则miss(说明是其他地址的数据)

另外:

  1. 实际上每个cacheline的tag还有一个valid bit,这个bit用来表示cacheline中数据是否有效(1有效;0是无效)。实际在确认hit之前,会检查该位是否有效,有效tag才有意义,否则无效,直接判断失效。
  2. data ram最后会有一个dirty bit,用于​​标记数据是否被修改且未同步回主存,在写回策略发挥关键作用,后面在详细说明下。

直接映射缓存在硬件设计上会更加简单,成本上也会较低。根据直接映射缓存的工作方式,我们可以画出主存地址地址和Cache的对应的关系图,如下:
在这里插入图片描述
可以看到数据块0-7地址对应数据刚好对应整个Cache,数据块8-f的地址也是同样对应整个Cache。
但这种方式在使用时可能带来以下问题:

  • 1.高冲突
    比如数据块0、8都是对应cacheline0,恰好访问在数据0和8之前切换,则会造成Cache 未满,热点数据仍被频繁驱逐,命中率骤降。
  • 2.资源利用率低
    比如频繁访问数据块3,则cacheline3一直被占用,而其他cacheline一直限制
  • 3.性能波动
    比如部分程序内存地址分布问题,导致访问冲突率高,会导致性能下降。
    以上使得现代处理器很少使用这种方式。

2.2.2 组相连映射缓存

当然组相连就是为了解决直接映射不足,在**组相联缓存(Set-Associative Cache)**中,一个数据可以存储在多个cacheline中,具体位置由组索引(Set Index)决定。如果一个数据可以映射到同一组内的​​n个不同的Cache Line​​,那么这个缓存就称为​​n路组相联(n-way set-associative)缓存​​。

例如:
​​2路组相联​​:每个组(Set)有2个Cache Line,数据可放在其中任意一个。
​​4路组相联​​:每个组有4个Cache Line,数据可存放在其中任意一个。
这种设计减少了冲突失效(Conflict Miss),提高了缓存命中率,相对增加些硬件复杂度(如并行比较多个Tag)。
在这里插入图片描述
原理:

  • 1.地址解析:当CPU请求数据时,首先将地址分解为三部分:标记(Tag)、组索引(Set Index)和偏移(offset)
  • 2.组定位:利用组索引确定数据位域哪个组。每个组包含多个缓存行(cacheline)
  • 3.标记比较:在确定的组内,CPU比较请求地址的标记和组内tag ram的标记(valid为高时),寻找匹配的缓存行
  • 4.命中处理:如果找到匹配的标记,表示缓存命中(hit),则根据偏移直接从缓存中读取数据
  • 5.未命中处理:若无匹配,则缓存未命中(miss),需要从主存加载数据到缓存中

根据组相连映射缓存的工作方式,我们可以画出主存地址地址和Cache的对应的关系图,如下:
在这里插入图片描述

2.2.3 全相连映射缓存

在**全相连缓存(Fully Associative Cache)**中,存储器中的任意地址可以存放在​​任意一个Cache Line​​中,无需通过索引(Index)定位。此时,地址仅包含​​Tag​​和​​块内偏移(Block Offset)​​,系统会并行比较所有Cache Line的Tag(通常使用​​CAM​​(内容寻址存储器)存储Tag,而Data部分仍用普通SRAM存储)。由于需要全局匹配,全相连缓存的​​灵活性最高​​(无冲突失效),但​​硬件开销大、延迟高​​,因此通常仅用于​​小容量高速缓存​​(如TLB)。
在这里插入图片描述
在实际硬件设计中,更常用的是组相连映射方式,缓存的分组策略(特别是​​组相联缓存​​的关联度选择)会紧密围绕 ​​Cache 总容量、访问延迟要求及成本约束​​进行综合权衡:

​​分组(n值)与 Cache 容量的关系​​:

  • ​​小容量 Cache​​(如 L1 Cache:32–64KB):→ 采用 ​​4–8 路组相联​​(如 Intel L1: 8-way)。理由:容量有限时需较高关联度缓解冲突失效(避免频繁替换热点数据)。
  • ​​大容量 Cache​​(如 L3 Cache:10–100MB):→ 采用 ​​12–24 路组相联​​(如 AMD Zen4: 16-way,Apple M2: 24-way)。理由:容量本身降低冲突概率,但须控制延迟 → 中等关联度 + 分Bank设计。

L1、L2、L3 Cache常见的分组:

  • L1 Cache​​(延迟敏感):8-way(Intel/ARM)或 4-way(部分嵌入式芯片)。
  • L2 Cache​​(容量/延迟平衡):8–12 way(通用CPU)或 16-way(Apple M系列)。
  • L3 Cache​​(容量优先):12–24 way(通过多组Bank并行降低高关联度延迟)。

实际现在很多设计都会把Cache做成可配的,比如把Cache配置为ram使用,或者一半配成ram另外一半配置为Cache

3 cache更新策略

cache更新策略是指当发生cache命中时,写操作应该如何更新数据。

  • cache写策略(Write Policy),缓存命中时的写入方式分成两种:写直通((Write-Through)​)和回写(Write-Back)。

  • 写分配策略(Write Allocation Policy), 缓存未命中时的决策分为:写分配(Write-Allocate)和非写分配(Non-Write-Allocate)

  • 读分配策略(Read-Allocation Policy),即缓存未命中时的读取方式分为:读分配(Read-Allocate)和非读分配(Non-Read-Allocate)​

而读命中,一般没有名字,直接从Cache中读取。

下面时Read/Write在Hit/Miss情况下,不同策略的表现行为和优缺点:

行为Hit/Miss类型解释优缺点
ReadHit-CPU 直接从 Cache 获取数据。优点:极快(纳秒级延迟)。
MissRead Through数据直接从 Main Memory 读取到 CPU,不存入 Cache优点:避免 Cache 污染。
缺点:重复读取相同数据效率低。
Read Allocate数据从 Main Memory 加载到 Cache,再从 Cache 读取到 CPU。优点:后续读取相同数据更快。
缺点:占用 Cache 空间。
No-Read Allocate等同于 Read Through,数据不加载到 Cache。同 Read Through。
WriteHitWrite Through数据同时写入 Cache 和 Main Memory。优点:内存数据始终最新(强一致性)。
缺点:写入延迟高,带宽压力大。
Write Back数据仅写入 Cache,Main Memory 延迟更新(如 Cache 替换时)。优点:写入速度快,带宽占用低。
缺点:内存数据可能过期(需额外机制维护一致性)。
MissWrite Allocate数据先加载到 Cache,再按 Write Hit 策略处理(Write Through/Back)。优点:适合后续可能重复写入的场景。
缺点:额外加载开销。
No-Write Allocate数据直接写入 Main Memory,不加载到 Cache。优点:避免无效 Cache 占用。
缺点:后续写入无加速。

4 Cache使用问题

4.1 性能问题

const int row = 1024;
const int col = 1024;
int matrix[row][col];//按行遍历
int sum_row = 0;
for (int r = 0; r < row; r++) {for (int c = 0; c < col; c++) {sum_row += matrix[r][c];}
}//按列遍历
int sum_col = 0;
for (int c = 0; c < col; c++) {for (int r = 0; r < row; r++) {sum_col += matrix[r][c];}
}

上面是个老生常谈的代码,Cache最小操作单元是cacheline,根据局部性原理,访问主存时会把相邻的部分也加载到Cache里,按行访问的话,后续访问地址相邻的数据时,Cache的命中率就会很高,性能相应按列访问会有不小提升。

4.2 缓存一致性问题

  1. 写直达(Write Through)
    我们在写内存时,如果是写直达(Write Through)方式,把数据同时写入内存和 Cache 中,那么内存和Cache就直接保持一致,就不会存在一致性问题。
    但这样优点就是简单,但缺点是无论数据在不在 Cache 里面,每次写操作都会写回到内存,这样写操作将会花费大量的时间,无疑性能会受到很大的影响。

  2. 写回(Write Back)
    在写回机制中,当发生写操作时,新的数据仅仅被写入 Cache Block 里,只有当修改过的 Cache Block「被替换」时才需要写到内存中,减少了数据写回内存的频率,这样便可以提高系统的性能。

  3. 缓存命中时(数据在 Cache 中)​​

  • 直接更新 Cache 数据
  • 标记 Cache Block 为​​脏(Dirty)​​ → 代表与内存不一致
  • 但不写入内存

2.缓存未命中时(数据不在 Cache 中)​​
​​- 若目标 Cache Block 是脏的​​:

  • 先将该 Block 数据​​写回内存​​
  • 再加载新数据到 Cache
  • 标记为脏
    ​- ​若目标 Cache Block 是干净的​​:
  • 直接加载新数据到 Cache
  • 标记为脏

会发现这种好处是,在大量操作命中Cache的时,大部分时间CPU都不需要读写内存,性能将可以提升很多。

场景1:
单核缓存一致性问题
在这里插入图片描述
1、DMA将数据包写入内存mem,CPU(协议栈)需要读取该数据包,但Cache并感知不到内存更新,这是就会导致不一致的问题;
2、同理,CPU(协议栈)组包之后,写入mem,实际只写入了Dcache,并未写入到内存,此时DMA去读取,获取到的也是旧数据。

解决方法:
一般这种单核和外设之间的数据一致性问题,也比较简单,可以通过软件来操作保证缓存一致性:
1、CPU读时数据标记缓存无效,将会把数据先读取到Cache,再从Cache读走。
2、CPU写数据时,刷(flush)Cache操作,将数据从cache刷到mem中。
当然也可以把Cache关了,读写直通(虽然没了一致性问题,但就相当于没了Cache)

场景2:
多核一致性问题
现在的CPU很多都是多核,每个Core都会拥有各自的L1 Cache,每个Cluster也有有自己的L2 Cache,所有的Core也会有L3 Cache,那么就会带来多核的缓存一致性的问题,如果不能保证缓存一致性的问题,就可能给软件带来很多意想不到的错误。
在这里插入图片描述
当前的Cluster里共两个core,当core0修改全局地址变量 i = 10;然后缓存到了Cache(并未修改mem),此时core1读取i,获取到的仍然时未修改前的变量。(这里只有L1 是各自core独有的,L2和L3 Cache是俩个core共有)

为了解决这个问题,就要一种机制来同步俩个core 缓存中的数据,这个机制就要做到以下两点:
1、写传播(Wreite Propagation),即:某个core的Cache数据更新时,必须要传播到其他core的Cache
2、事务的串形化(Transaction Serialization),即:某个core里对数据的操作顺序,必须在其他核心看起来顺序是一样的(这里实现需要“锁”机制)

总线嗅探(Snooping)
原理​​:所有缓存通过共享总线监听内存操作(如读写请求),嗅到相关地址时触发本地动作。

MESI​​协议
最经典的协议是MESI

MESI 协议其实是 4 个状态单词的开头字母缩写,分别是:

  • Modified,已修改
  • Exclusive,独占
  • Shared,共享
  • Invalidated,已失效

这四个状态来标记cacheline四个不同的状态,即cacheline的状态机。

状态含义监听动作触发
M (Modified)数据已修改(仅本核心有效)收到读请求 → 写回内存并转S
E (Exclusive)独占(未修改,仅本核有)收到写请求 → 转 M
S (Shared)共享(多核只读副本)收到写请求 → 无效化本副本
I (Invalid)无效(数据不可用)可加载新数据

简单流程:

在这里插入图片描述在这里插入图片描述

当然我这里只是简单介绍了下保证缓存一致性的机制,感兴趣,可以自行搜索深度学习下,作为软件开发,想要提升软件性能,发挥硬件的最大能力,还是要理解这些硬件机制的。

参考:
Cache 学习笔记
浅析CPU高速缓存(cache)
一文看懂CPU cache的基本原理

http://www.dtcms.com/a/332085.html

相关文章:

  • 基于大语言模型的爬虫数据清洗与结构化
  • 可信搜索中的多重签名
  • 系统日常巡检脚本
  • 将mysql数据库表结构导出成DBML格式
  • Qt---Qt函数库
  • ActionChains 鼠标操作笔记
  • # Vue 列表渲染详解
  • AI智能体|扣子(Coze)搭建【批量识别发票并录入飞书】Agent
  • FTP 服务详解:原理、配置与实践
  • 8月14日星期四今日早报简报微语报早读
  • [激光原理与应用-273]:理论 - 波动光学 - 光是电磁波,本身并没有颜色,可见光的颜色不过是人的主观感受
  • 时钟 中断 day54
  • close函数概念和使用案例
  • rustdesk 开源遥控软件
  • 云服务器运行持续强化学习COOM框架的问题
  • 低配硬件运行智谱GLM-4.5V视觉语言模型推理服务的方法
  • C#WPF实战出真汁01--项目介绍
  • linux设备驱动之USB驱动-USB主机、设备与Gadget驱动
  • 【Java|第十九篇】面向对象九——String类和枚举类
  • AI更换商品背景,智能融合,无痕修图
  • Java中加载语义模型
  • Windows bypassUAC 提权技法详解(一)
  • 洗浴中心泡池水过滤系统原理深度解析与工程实践
  • RocketMQ 4.9.3源码解读-客户端Consumer消费者组件启动流程分析
  • 具身智能Scaling Law缺失:机器人界的“摩尔定律“何时诞生?
  • Ansible企业级实战
  • centos部署chrome和chromedriver
  • C#WPF实战出真汁03--登录界面设计
  • C#WPF实战出真汁04--登录功能实现
  • 单目操作符与逗号表达式