为什么需要高速缓存
CPU和内存的性能差距会越来越大.png按照摩尔定律、CPU的访问速度每18个月便会翻一番、相当于每年增长60%、内存的访问速度也在增长、但只有7%左右、使内存和CPU的性能差距不断拉大. 现在一次内存的访问需要120个CPU周期、即: 访问速度有120倍的差距
为了弥补两者的性能差异、真实的把CPU使用起来、现代CPU引入了高速缓存
从CPU Cache 被加入CPU开始、内存中的指令、数据会先加载到L1
-L3 Cache中、而不是直接由CPU访问内存去拿、
在95%的情况下、CPU只需要访问L1-L3 Cache、从里边读取指令和数据、无需访问内存
注意: CPU Cache不是单存概念上的Cache(eg. 之前说的以内存作为硬盘的缓存)、而是指特定的由SRAM组成的物理芯片
CPU从内存中读取数据到CPU Cache的过程中、是一小块、一小块读取的、不是按照单个数组元素读取的、
这样一小块一小块的数据、在CPU Cache里、称为 `Cache Line`(缓存块)
通常Intel的服务器或者PC里、Cache Line的大小通常是64字节
Cache的数据结构和读取过程是什么样的
现代CPU在数据读取时、无论数据是否已经存储在Cache中、都会先访问Cache、
只有Cache中找不到时、才会访问内存、并且将读到的数据写入Cache中、根据时间局部性原理、这样CPU花在等待内存访问上的时间就会大大缩短
通常在基准测试和实际场景中、CPU Cache的命中率可以达到95%以上
与应用相似的架构.png
思考: 那么CPU是如何知道要访问的内存数据、存储在Cache的哪个位置呢 ?
CPU访问内存数据、是一小块一小块数据来读取的、对于读取内存中的数据、首先拿到的是数据所在的`内存块(Block)`的地址
而直接映射Cache(Direct Mapped Cache)采用的策略就是确保任何一个内存块的地址、
始终映射到一个固定的CPU Cache地址(Cache Line), 而这个映射关系、通常用mod(求余)计算来实现
eg. 将主内存分成0~31共32个块、共有8个缓存块、用户需要访问第21号内存块、
若21号内存块内容在缓存块中的话、一定在5号缓存块(21%8 = 5)
cache采用mod方式、将内存块映射到对应的CPU Cache.png
思考:
现在13号、5号和21号都应该在5号缓存块中、那如何区分、当前缓存块中是几号内存对应的数据呢 ?
对应的缓存块会存储一个组标记、记录当前缓存块内存储的数据对应的内存块、
除组标记外、缓存块中还有两个数据:
1. 从主内存中加载来的实际存放的数据、
2.有效位: 用来标记对应的缓存块中的数据是否是有效的、确保不是机器刚启动时的空数据. 有效位为0时、CPU会直接访问内存、重新加载数据
CPU在读取数据的时候、不是要读取医政股Block、只读取一个需要的整数、称为CPU的一个字Word
具体是哪个字、就用这个字在整个Block中的位置来决定、这个位置称为偏移量Offset
而内存地址对应到Cache Line里的数据结构则多了一个有效位和对应的数据
由 索引 + 有效位 + 组标记 + 数据组成
若: 一个内存中的数据已经在Cache中、访问步骤为:
1. 根据内存地址的地位、计算在Cache中的索引
2. 判断有效位、确认Cache中的数据是有效的
3. 对比内存访问地址的高位、和Cache中的组标记、确认Cache中的数据就是要访问的内存数据、从Cache Line读取对应数据块
4. 根据内存地址的Offset位、从Data Block中读取所需内容
若在2、3步骤中CPU发现数据不是要访问的数据、CPU会访问内存、将Block Data更新到Cache Line中、同时更新对应的有效位和组标记的数据
内存地址到Cache Line的关系.png
思考:volitile 关键字的作用 ?
一种错误理解是 当成锁、认为类似sychronized 关键字、不同的线程对于特定变量的访问会加锁
另一种误解是 当成原子化的操作机制、认为加了volitile、对于变量的自增操作就变成原子性的了
其实: volitile 最核心的知识点要关系到Java内存模型(Java Memory Model)上
虽然JMM只是Java虚拟机这个进程级虚拟机里的一个内存模型、但这个内存模型和CPU、高速缓存和主内存组合在一起的硬件体系很相似
虽然Java内存模型是一个隔离了硬件实现的虚拟机内的抽象模型、但给了一个很好的缓存同步问题的示例.
即: 若我们的数据在不同的线程或者CPU核心里去更新、因为不同的线程或者CPU核有各自的缓存、有可能在A线程的更新B线程看不到
CPU高速缓存的写入
现在使用的Intel CPU、通常都是多核的、每一个CPU核里、都有独立属于自己的L1、L2的Cache 和 多个CPU共用的L3的Cache、主内存
因为CPU Cache的访问速度要比主内存快很多、L1/L2 Cache的速度也比L3 快、所以、CPU都是尽可能的从CPU Cache中获取数据、而不是每次从主内存获取
Java内存模型里、每个线程都有自己的线程桟、每次数据读取其实是从本地线程桟的Cache副本里读取、而不是主内存
若对数据只读还好、但事实是读写同时存在的、这时思考两个问题:
- 写Cache的性能也比写主内存快、那写入时应该写主内存还是Cache ?
2.若直接写主内存、Cache里的数据是否会失效呢 ?
写直达 Write-Through
image.png如上图、最简单的一种写入策略, 写直达. 在这个策略里、每一次数据都要写到主内存里、
写入前, 先判断数据是否在Cache中、若在、先更新Cache、再写入主内存; 若不在、只更新主内存
缺点是: 无论数据是否在Cache里、都需要把数据写到主内存、效率较低
类似volatile关键字、始终把数据同步到主内存里
写回 Write-back
image.png不把写入同步到主内存、只写CPU Cache呢?是否可行 ?
如上图所示就是只写Cache的策略 -> 每次只把数据写回Cache、只有在CPU Cache里的数据要被替换的时候、才将数据写回主内存
过程: 若发现要写回的数据就在Cache中、只更新Cache里的数据即可、同时标记Cache里这个Block的数据是Dirty(与主存不一致)
若发现要写入的数据对应的Cache Block里、放的是别的内存地址的数据
check下数据是否是Dirty、
若是、先写回主内存然后把当前要写入的数据写入到Cache、同时把Cache Block标记成Dirty
若不是、直接将数据写入Cache、然后把Cache Block标记为Dirty
在使用`写回`这个策略的时候、加载内存数据到Cache里的时候、也要多一步同步脏Cache的动作
若加载内存里的数据到Cache的时候、发现Cache Block里有Dirty标记、需要先把Cache Block的数据写回主内存、才能加载数据覆盖掉Cache
在写回这个策略里、若大量的操作、都能命中缓存、大部分时间都不需要读写主内存、性能会比写直达好
然而、无论写直达还是写回、都未解决多线程或者多CPU缓存一致性问题....
网友评论