美文网首页
高速缓存(37~38讲)

高速缓存(37~38讲)

作者: 牛牛_735d | 来源:发表于2020-01-13 21:42 被阅读0次

    为什么需要高速缓存

    按照摩尔定律、CPU的访问速度每18个月便会翻一番、相当于每年增长60%、内存的访问速度也在增长、但只有7%左右、使内存和CPU的性能差距不断拉大. 现在一次内存的访问需要120个CPU周期、即: 访问速度有120倍的差距
    为了弥补两者的性能差异、真实的把CPU使用起来、现代CPU引入了高速缓存

    CPU和内存的性能差距会越来越大.png
    从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副本里读取、而不是主内存
    若对数据只读还好、但事实是读写同时存在的、这时思考两个问题:
    
    1. 写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缓存一致性问题....

    相关文章

      网友评论

          本文标题:高速缓存(37~38讲)

          本文链接:https://www.haomeiwen.com/subject/lglbactx.html