03.计算机基础CPU设计
目录介绍
- 01.认识CPU处理器
- 1.1 什么是CPU
- 1.2 为何学习CPU
- 1.3 通用&专用处理器
- 1.4 CPU和GPU
- 1.5 思考一些问题
- 02.CPU指令集架构
- 2.1 CPU指令集架构
- 2.2 两种主流指令集
- 2.3 复杂&精简指令集
- 03.CPU的性能指标
- 3.1 执行系统参数
- 3.2 存储系统参数
- 04.影响CPU性能因素
- 4.1 提升CPU主频
- 4.2 多核并行执行
- 4.3 指令重排序
- 05.缓存一致性设计
- 5.1 通过案例看CPU读写
- 5.2 指令重排解决什么
- 5.3 缓存一致性问题
- 5.4 通过案例理解一致性
- 5.5 解决一致性总思路
- 06.纵向Cache和内存一致性
- 6.1 Cache 的读取过程
- 6.2 写直达策略(Write-Through)
- 6.3 写回策略(Write-Back)
- 07.横向多核心Cache一致性问题
- 7.1 多核 Cache 不一致
- 7.2 写传播 & 事务串行化
- 7.3 总线嗅探 & 总线仲裁
- 7.4 MESI 协议
- 7.5 写缓冲区 & 失效队列
- 08.什么是伪共享
- 8.1 并发伪共享
- 8.2 如何解决伪共享
- 8.3 并发集合解决伪共享
01.认识CPU处理器
1.1 什么是CPU
- 如何理解CPU呢
- 它是中央处理单元(Central Processing Unit,CPU),也叫中央处理器或主处理器,是整个计算机的核心,也是整台计算机中造价最昂贵的部件之一。
- 从硬件的角度:
- CPU 由超大规模的晶体管组成;
- 从功能的角度: CPU 内部由时钟、寄存器、控制器和运算器 4 大部分组成。
- 1、时钟(Clock): 负责发出时钟信号,也可以位于 CPU 外部;
- 2、寄存器(Register): 负责暂存指令或数据,位于存储器系统金字塔的顶端。使用寄存器能够弥补 CPU 和内存的速度差,减少 CPU 的访存次数,提高 CPU 的吞吐量;
- 3、控制器(Control Unit): 负责控制程序指令执行,包括从主内存读取指令和数据发送到寄存器,再将运算器计算后的结果写回主内存;
- 4、运算器(Arithmetic Logic Unit,ALU): 负责执行控制器取出的指令,包括算术运算和逻辑运算。
- 冯·诺依曼架构
image
1.2 为何学习CPU
- 日常所处理的工作都是在跟 Java 和 C++ 等高级语言打交道,并不会直接地与 CPU 打交道。那么,为什么我们还要花这么多时间去学习 CPU 呢?我认为有以下原因:
- 掌握 CPU 原理能够开发更高性能的程序: 理解 CPU 的工作原理有助于设计出更高性能的算法或代码,例如通过避免伪共享、提高缓存命中率等方式提高程序运行效率,就需要对 CPU 的缓存机制有一定的理解;
- 扩展方案积累: CPU 是整个计算机系统中最复杂的模块,也是当代计算机科学的制高点。积累 CPU 内部的解决方案,能够为将来的遇到类似问题提供思路,达到触类旁通的作用。例如 CPU 缓存淘汰策略与应用内存的缓存淘汰策略有相似之处;
- CPU 是知识体系最底层的知识: 当我们在思考或解决某一个问题时,就需要利用到更深层次的知识积累来解释,而 CPU 就是位于知识体系中最底层知识。例在内存系统的可见性、线程池设计等问题中,都需要对 CPU 的执行机制有一定理解。
1.3 通用&专用处理器
- 在早期的计算机系统中,只有 1 个通用处理器,使用 1 个处理器就能够完成所有计算任务。后来人们发现可以把一些计算任务分离出来,单独设计专门的芯片微架构,在执行效率上会远远高于通用处理器,最典型的专用处理器就是 GPU 图形处理器。
- 这种用来专门处理某种计算任务的处理器就是专用处理器,那为什么专用处理器在处理某些特定问题时更快呢,我认为有 3 点解释:
- 1、最优架构: 专用处理器只处理少量类型的工作,可以为特定工作设计最优芯片架构,而通用处理器只能设计全局最优架构,但不一定是执行特定工作的最优机构;
- 2、硬件加速: 可以把多条指令的计算工作直接用硬件实现,相比于 CPU 一条条地执行指令,能够节省大量指令周期;
- 3、成本更低: 专用处理器执行的计算流程是固定的,不需要 CPU 的流水线控制、乱序执行等功能,实现相同计算性能的造价更低。
1.4 CPU和GPU
- CPU是计算机的主要处理器,负责执行计算机的大部分任务。
- 它是一个通用处理器,可以执行各种不同类型的指令,包括算术、逻辑、控制和输入/输出操作。
- CPU通常具有较少的核心(通常为几个核心),但每个核心都非常强大,可以处理复杂的任务。CPU在操作系统、应用程序和数据处理方面发挥着重要作用。
- GPU是专门用于图形处理的处理器。
- 它主要用于处理图形和图像相关的任务,如图形渲染、图像处理、视频解码等。
- GPU具有大量的并行处理单元,可以同时处理多个任务。这使得GPU在处理大规模数据和并行计算方面非常高效。GPU在游戏、计算机图形学、科学计算和人工智能等领域发挥着重要作用。
- 一句话概括CPU和GPU的功能
- CPU负责控制和协调计算机的整体运行,处理通用任务和复杂的逻辑操作。而GPU则专注于处理图形和并行计算任务,通过并行处理提供更高的计算性能。
1.5 思考一些问题
- 思考一些问题
- 01.CPU优化中,主要是涉及到那些点的优化?如何进行优化和衡量标准?
- 02.CPU主要是跟线程有关吗?如何理解CPU?如何搞懂CPU架构?
- 一些核心概念
- CPU,这里也可以叫做CPU寄存器。一般计算器是多核的,有多个CPU,主要是为了提高计算效率!
02.CPU指令集架构
2.1 CPU指令集架构
- CPU 所能理解的机器语言就是 指令(Instruction Code), 一个 CPU 所能理解的所有指令就是 指令集(Instruction Set)。
- 为了保证芯片间的兼容性,芯片厂商并不为每款新芯片设计一个新的指令集,而是将指令集推广为标准规范,这个规范就是 指令集架构(Instruction Set Architecture,ISA) ,
- 相对于指令集架构,CPU 在实现具体指令集功能的硬件电路设计就是 微架构(Micro Architecture)。
- 如果用软件的思考方式,ISA 就是 CPU 的功能接口,定义了 CPU 的标准规范,而微架构就是 CPU 的功能实现,定义了 CPU 的具体电路设计,一种指令集可以兼容不同的微架构。
2.2 两种主流指令集
- 因为 CPU 位于整个计算机系统最底层且最核心的部件,如果 CPU 的兼容性都出问题了,那么以前开发的应用软件甚至操作系统将无法在新的 CPU 上运行,这对芯片厂商的生态破坏是致命的。因此,指令集架构是相对稳定的,芯片厂商在 ISA 中添加或删除指令时会非常谨慎。
- 目前,能够有效占领市场份额的只有 2 个 ISA ,它们也分别代表了复杂与精简 2 个发展方向:
- x86 架构: Intel 公司在 1970 年代推出的复杂指令集架构;
- ARM 架构: ARM 公司在 1980 年代推出的精简指令集架构,我们熟悉的 Apple M1 芯片、华为麒麟芯片和高通骁龙芯片都是 ARM 架构
2.3 复杂&精简指令集
- 在 CPU 指令集的发展过程中,形成了 2 种指令集类型:
- 复杂指令集(Complex Instruction Set Computer,CISC): 强调单个指令可以同时执行多个基本操作,用少量指令就可以完成大量工作,执行效率更高;
- 精简指令集(Reduced Instruction Set Computer,RISC): 强调单个指令只能执行一个或少数基础操作,指令之间没有重复或冗余的功能,完成相同工作需要使用更多指令。
- 在早期的计算机系统中,指令集普遍很简单,也没有复杂和精简之分。
- 随着应用软件的功能越来越丰富,应用层也在反向推动芯片架构师推出更强大的指令集,以简化程序编写和提高性能。例如,一些面向音视频的指令可以在一条指令内同时完成多个数据进行编解码。
- CPU 和主存的速度差实在太大了,用更少的指令实现程序功能(指令密度更高)可以减少访存次数。
- 复杂指令集对精简指令集的优势是几乎全面性的:
- 优势 1: 可以减少程序占用的内存和磁盘空间大小;
- 优势 2: 可以减少从内存或磁盘获取指令所需要的带宽,能够提高总线系统的传输效率;
- 优势 3: CPU L1 Cache 可以容纳更多指令,可以提高缓存命中率。且现代计算机中多个线程会共享 L1 Cache,指令越少对缓存命中率越有利;
- 优势 4: CPU L2 Cache 可以容纳更多数据,对操作大量数据的程序也有利于提高缓存命中率。
- 这些优势都是有代价的:
- 缺点 1 - 处理器设计复杂化: 指令越复杂,用于解析指令的处理器电路设计肯定会越复杂,执行性能功耗也越大;
- 缺点 2 - 指令功能重叠: 很多新增的指令之间产生了功能重叠,不符合指令集的正交性原则,而且新增的很多复杂指令使用率很低,但处理器却付出了不成正比的设计成本;
- 缺点 3 - 指令长度不统一: 指令长度不统一,虽然有利于使用哈夫曼编码进一步提高指令密度(频率高的指令用短长度,频率高的指令用大长度),但是指令长度不同,执行时间也有长有短,不利于实现流水线式结构。
03.CPU的性能指标
3.1 执行系统参数
- 1、主频(Frequency/Clock Rate):
- 在 CPU 内部有一个 晶体振荡器(Oscillator Crystal) ,晶振会以一定的频率向控制器发出信号,这个信号频率就是 CPU 的主频。
- 主频是 CPU 最主要的参数,主频越快,计算机单位时间内能够完成的指令越快。
- CPU 的主频并不是固定的,CPU 在运行时可以选择低频、满频甚至超频运行, 但是工作频率越高,意味着功耗也越高;
- 2、时钟周期(Clock Cycle):
- 主频的另一面,即晶振发出信号的时间间隔, 时钟周期=1/主频;
- 3、外频:
- 外频是主板为 CPU 提供的时钟频率,早期计算机中 CPU 主频和外频是相同的,但随着 CPU 主频越来越高,而其他设备的速度还跟不上,所以现在主频和外频是不相等的;
- 4、程序执行时间:
- 4.1 流逝时间(Wall Clock Time / Elapsed Time): 程序开始运行到程序结束所流逝的时间;
- 4.2 CPU 时间(CPU Time): CPU 实际执行程序的时间,仅包含程序获得 CPU 时间片的时间(用户时间 + 系统时间)。由于 CPU 会并行执行多个任务,所以程序执行时间会小于流逝时间;
- 4.3 用户时间(User Time): 用户态下,CPU 切换到程序上执行的时间;
- 4.4 系统时间(Sys Time): 内核态下,CPU 切换到程序上执行的时间;
3.2 存储系统参数
- 字长(Word):
- CPU 单位时间内同时处理数据的基本单位,多少位 CPU 就是指 CPU 的字长是多少位,比如 32 位 CPU 的字长就是 32 位,64 位 CPU 的字长就是 64 位;
- 地址总线宽度(Address Bus Width):
- 地址总线传输的是地址信号,地址总线宽度也决定了一个 CPU 的寻址能力,即最多可以访问多少数据空间。举个例子,32 位地址总线可以寻址 4GB 的数据空间;
- 数据总线宽度(Data Bus Width):
- 数据总线传输的是数据信号,数据总线宽度也决定了一个 CPU 的信息传输能力。
- 区分其它几种容量单位:
- 字节(Byte): 字节是计算机数据存储的基本单位,即使存储 1 个位也需要按 1 个字节存储;
- 块(Block): 块是 CPU Cache 管理数据的基本单位,也叫 CPU 缓存行;
- 段(Segmentation)/ 页(Page): 段 / 页是操作系统管理虚拟内存的基本单位。
04.影响CPU性能因素
- 1、提升 CPU 性能不止是 CPU 的任务: 计算机系统是多个部件组成的复杂系统,脱离整体谈局部没有意义;
- 2、平衡性能与功耗: 一般来说,CPU 的计算性能越高,功耗也越大。我们需要综合考虑性能和功耗的关系,脱离功耗谈性能没有意义。
4.1 提升CPU主频
- 提升主频对 CPU 性能的影响是最直接的,过去几十年 CPU 的主要发展方向也是在怎么提升 CPU 主频的问题上。
- 最近几年 CPU 主频的速度似乎遇到瓶颈了。因为想要主频越快,要么让 CPU 满频或超频运行,要么升级芯片制程,在单位体积里塞进去更多晶体管。
- 这两种方式都会提升 CPU 功耗,带来续航和散热问题。如果不解决这两个问题,就无法突破主频瓶颈。
4.2 多核并行执行
- 既然单核 CPU 的性能遇到瓶颈,那么在 CPU 芯片里同时塞进去 2 核、4 核甚至更多,那么整个 CPU 芯片的性能不就直接翻倍提升吗?
- 理想很美好,现实是性能并不总是随着核心数线性增加。在核心数较小时,增加并行度得到的加速效果近似于线性提升,但增加到一定程度后会趋于一个极限, 说明增加并行度的提升效果也是有瓶颈的。
- 为什么呢?因为不管程序并行度有多高,最终都会有一个结果汇总的任务,而汇总任务无法并行执行,只能串行执行。例如,我们用 Java 的 Fork/Join 框架将一个大任务分解为多个子任务并行执行,最终还是需要串行地合并子任务的结果。
4.3 指令重排序
- 现代 CPU 为了提高并行度,会在遵守单线程数据依赖性原则的前提下,对程序指令做一定的重排序。事实上不止是 CPU,从源码到指令执行一共有 3 种级别重排序:
- 1、编译器重排序: 例如将循环内重复调用的操作提前到循环外执行;
- 2、处理器系统重排序: 例如指令并行技术将多条指令重叠执行,或者使用分支预测技术提前执行分支的指令,并把计算结果放到重排列缓冲区(Reorder Buffer)的硬件缓存中,当程序真的进入分支后直接使用缓存中的结算结果;
- 3、存储器系统重排序: 例如写缓冲区和失效队列机制,即是可见性问题,从内存的角度也是指令重排问题。
- 指令重排序类型
image
05.缓存一致性设计
5.1 通过案例看CPU读写
- CPU寄存器读和写数据
- 当CPU需要读取主内存的时候,他会将部分数据读到CPU缓存中,甚至可以将CPU缓存中的部分数据读到寄存器中,然后在寄存器中操作,操作完成后,需要将数据写入主存中的时候,先将数据刷新至CPU缓存中,然后在某个时间点将数据刷新到主存中。
- 当CPU需要在缓存层存放一些东西的时候,存放在缓存中的内容通常会被刷新回主存。CPU缓存可以在某一时刻将数据局部写到它的内存中,和在某一时刻局部刷新它的内存。它不会再某一时刻读/写整个缓存。
- 举一个案例理解CPU执行读写操作
- For example : int i = i + 1
- 当线程执行这个语句时,会先从主内存中读取i的值,然后复制一份到CPU的高速缓存中,然后CPU执行指令对i进行加1的操作,然后将数据写入高速缓存,最后将最新的i值刷新到主存当重。
5.2 指令重排解决什么
5.3 缓存一致性问题
- CPU 缓存一致性(Cache Coherence)问题指 CPU Cache 与内存的不一致性问题。
- 事实上, 在分析缓存一致性问题时,考虑 L1 / L2 / L3 的多级缓存没有意义, 所以我们提出缓存一致性抽象模型,只考虑核心独占的缓存。
- 在单核 CPU 中,只需要考虑 Cache 与内存的一致性。
- 在多核 CPU 中,由于每个核心都有一份独占的 Cache,就会存在一个核心修改数据后,两个核心 Cache 数据不一致的问题。
- 因此,我认为 CPU 的缓存一致性问题应该从 2 个维度理解:
- 纵向:Cache 与内存的一致性问题: 在修改 Cache 数据后,如何同步回内存?
- 横向:多核心 Cache 的一致性问题: 在一个核心修改 Cache 数据后,如何同步给其他核心 Cache?
5.4 通过案例理解一致性
- 现代 CPU 通常有多个核心
- 每个核心也都有自己独立的缓存(L1、L2 缓存),当多个核心同时操作同一个数据时,如果核心 2 在核心 1 还未将更新的数据同步回内存之前读取了数据,就出现了缓存不一致问题。
- 举个例子,假设线程 A 和线程 B 同时对一个变量执行 i++,就可能存在缓存不一致问题:
- 1、核心 A 和核心 B 从内存中加载了 i 的值,并且缓存到各自的高速缓存中,此时两个副本都为0;
- 2、核心 A 进行加一操作,副本值变成了 1,最后回写到主存中,主存中的值为 1;
- 3、核心 B 进行加一操作,副本值变成了 1,最后回写到主存中,主存中的值为 1;
- 4、最终主存的值为 1,而不是期望的 2。
5.5 解决一致性总思路
- 解决CPU缓存不一致性问题
- 锁总线:
- 锁总线是对整个内存加锁,在锁总线期间,其他处理器无法访问内存,可想而知会严重降低 CPU 性能。
- 缓存一致性协议:
- 「锁内存方案」相当于保证了整块内存的一致性,而「缓存一致性协议方案」本质上相当与一致性保护范围,从整块内存缩小为单个缓存行(缓存行是缓存的基本单元)。
- 缓存一致性协议提供了一种高效的内存数据管理方案。当 CPU 核心准备写数据时,如果发现操作的变量是共享变量(即在其他核心中也存在该变量的副本),就会通知其他核心该变量「缓存行」无效,需要重新从内存读取。
06.纵向Cache和内存一致性
6.1 Cache 的读取过程
- 讨论 Cache 的读取过程。事实上,Cache 的读取过程会受到 Cache 的写入策略影响,我们暂且用相对简单的 “写直达策略” 的读取过程:
- 1、CPU 在访问内存地址时,会先检查该地址的数据是否已经加载到 Cache 中(Valid bit 是否为 1);
- 2、如果数据在 Cache 中,则直接读取 Cache 块上的字到 CPU 中;
- 3、如果数据不在 Cache 中:
- 3.1 如果 Cache 已装满或者 Cache 块被占用,先执行替换策略,腾出空闲位置;
- 3.2 访问内存地址,并将内存地址所处的整个内存块写入到映射的 Cache 块中;
- 3.3 读取 Cache 块上的字到 CPU 中。
- CPU 不仅会读取 Cache 数据,还会修改 Cache 数据,这就是第 1 个一致性问题
- 在修改 Cache 数据后,如何同步回内存?有 2 种写入策略:写直达策略(Write-Through);写回策略(Write-Back)
6.2 写直达策略(Write-Through)
- 写直达策略是解决 Cache 与内存一致性最简单直接的方式: 在每次写入操作中,同时修改 Cache 数据和内存数据,始终保持 Cache 数据和内存数据一致:
- 1、如果数据不在 Cache 中,则直接将数据写入内存;
- 2、如果数据已经加载到 Cache 中,则不仅要将数据写入 Cache,还要将数据写入内存。
- 写直达的优点和缺点都很明显:
- 优点: 每次读取操作就是纯粹的读取,不涉及对内存的写入操作,读取速度更快;
- 缺点: 每次写入操作都需要同时写入 Cache 和写入内存,在写入操作上失去了 CPU 高速缓存的价值,需要花费更多时间。
6.3 写回策略(Write-Back)
- 既然写直达策略在每次写入操作都会写内存,那么有没有什么办法可以减少写回内存的次数呢?这就是写回策略:
- 1、写回策略会在每个 Cache 块上增加一个 “脏(Dirty)” 标记位 ,当一个 Cache 被标记为脏时,说明它的数据与内存数据是不一致的;
- 2、在写入操作时,我们只需要修改 Cache 块并将其标记为脏,而不需要写入内存;
- 3、那么,什么时候才将脏数据写回内存呢?—— 就发生在 Cache 块被替换出去的时候:
- 3.1 在写入操作中,如果目标内存块不在 Cache 中,需要先将内存块数据读取到 Cache 中。如果替换策略换出的旧 Cache 块是脏的,就会触发一次写回内存操作;
- 3.2 在读取操作中,如果目标内存块不在 Cache 中,且替换策略换出的旧 Cache 块是脏的,就会触发一次写回内存操作;
07.横向多核心Cache一致性问题
7.1 多核 Cache 不一致
- 在单核 CPU 中,我们通过写直达策略或写回策略保持了Cache 与内存的一致性。但是在多核 CPU 中,由于每个核心都有一份独占的 Cache,就会存在一个核心修改数据后,两个核心 Cache 不一致的问题。
- 举个例子:
- 1、Core 1 和 Core 2 读取了同一个内存块的数据,在两个 Core 都缓存了一份内存块的副本。此时,Cache 和内存块是一致的;
- 2、Core 1 执行内存写入操作:
- 2.1 在写直达策略中,新数据会直接写回内存,此时,Cache 和内存块一致。但由于之前 Core 2 已经读过这块数据,所以 Core 2 缓存的数据还是旧的。此时,Core 1 和 Core 2 不一致;
- 2.2 在写回策略中,新数据会延迟写回内存,此时 Cache 和内存块不一致。不管 Core 2 之前有没有读过这块数据,Core 2 的数据都是旧的。此时,Core 1 和 Core 2 不一致。
- 3、由于 Core 2 无法感知到 Core 1 的写入操作,如果继续使用过时的数据,就会出现逻辑问题。
- 多核 Cache 不一致
- 由于两个核心的工作是独立的,在一个核心上的修改行为不会被其它核心感知到,所以不管 CPU 使用写直达策略还是写回策略,都会出现缓存不一致问题。
- 所以,我们需要一种机制,将多个核心的工作联合起来,共同保证多个核心下的 Cache 一致性,这就是缓存一致性机制。
7.2 写传播 & 事务串行化
- 缓存一致性机制需要解决的问题就是 2 点:
- 特性 1 - 写传播(Write Propagation): 每个 CPU 核心的写入操作,需要传播到其他 CPU 核心;
- 特性 2 - 事务串行化(Transaction Serialization): 各个 CPU 核心所有写入操作的顺序,在所有 CPU 核心看起来是一致。
- 第 1 个特性解决了 “感知” 问题,如果一个核心修改了数据,就需要同步给其它核心,很好理解。但只做到同步还不够,如果各个核心收到的同步信号顺序不一致,那最终的同步结果也会不一致。
- 举个例子:假如 CPU 有 4 个核心,Core 1 将共享数据修改为 1000,随后 Core 2 将共享数据修改为 2000。
- 在写传播下,“修改为 1000” 和 “修改为 2000” 两个事务会同步到 Core 3 和 Core 4。
- 但是,如果没有事务串行化,不同核心收到的事务顺序可能是不同的,最终数据还是不一致。
7.3 总线嗅探 & 总线仲裁
- 写传播和事务串行化在 CPU 中是如何实现的呢?—— 此处隆重请出计算机总线系统。
- 写传播 - 总线嗅探: 总线除了能在一个主模块和一个从模块之间传输数据,还支持一个主模块对多个从模块写入数据,这种操作就是广播。要实现写传播,其实就是将所有的读写操作广播到所有 CPU 核心,而其它 CPU 核心时刻监听总线上的广播,再修改本地的数据;
- 事务串行化 - 总线仲裁: 总线的独占性要求同一时刻最多只有一个主模块占用总线,天然地会将所有核心对内存的读写操作串行化。如果多个核心同时发起总线事务,此时总线仲裁单元会对竞争做出仲裁,未获胜的事务只能等待获胜的事务处理完成后才能执行。
- 基于总线嗅探和总线仲裁,现代 CPU 逐渐形成了各种缓存一致性协议,例如 MESI 协议。
7.4 MESI 协议
- MESI 协议其实是 CPU Cache 的有限状态机,一共有 4 个状态(MESI 就是状态的首字母):
- M(Modified,已修改): 表明 Cache 块被修改过,但未同步回内存;
- E(Exclusive,独占): 表明 Cache 块被当前核心独占,而其它核心的同一个 Cache 块会失效;
- S(Shared,共享): 表明 Cache 块被多个核心持有且都是有效的;
- I(Invalidated,已失效): 表明 Cache 块的数据是过时的。
- 如何理解
- 在 “独占” 和 “共享” 状态下,Cache 块的数据是 “清” 的,任何读取操作可以直接使用 Cache 数据;
- 在 “已失效” 和 “已修改” 状态下,Cache 块的数据是 “脏” 的,它们和内存的数据都可能不一致。在读取或写入 “已失效” 数据时,需要先将其它核心 “已修改” 的数据写回内存,再从内存读取;
- 在 “共享” 和 “已失效” 状态,核心没有获得 Cache 块的独占权(锁)。在修改数据时不能直接修改,而是要先向所有核心广播 RFO(Request For Ownership)请求 ,将其它核心的 Cache 置为 “已失效”,等到获得回应 ACK 后才算获得 Cache 块的独占权。这个独占权这有点类似于开发语言层面的锁概念,在修改资源之前,需要先获取资源的锁;
- 在 “已修改” 和 “独占” 状态下,核心已经获得了 Cache 块的独占权(锁)。在修改数据时不需要向总线发送广播,能够减轻总线的通信压力。
- 事实上,完整的 MESI 协议更复杂,但我们没必要记得这么细。我们只需要记住最关键的 2 点:
- 关键 1 - 阻止同时有多个核心修改的共享数据: 当一个 CPU 核心要求修改数据时,会先广播 RFO 请求获得 Cache 块的所有权,并将其它 CPU 核心中对应的 Cache 块置为已失效状态;
- 关键 2 - 延迟回写: 只有在需要的时候才将数据写回内存,当一个 CPU 核心要求访问已失效状态的 Cache 块时,会先要求其它核心先将数据写回内存,再从内存读取。
7.5 写缓冲区 & 失效队列
08.什么是伪共享
8.1 并发伪共享
- 在并行场景中,当多个处理器核心修改同一个缓存行变量时,有 2 种情况:
- 情况 1 - 修改同一个变量:
- 两个处理器并行修改同一个变量的情况,CPU 会通过 MESI 机制维持两个核心的缓存中的数据一致性(Conherence)。
- 简单来说,一个核心在修改数据时,需要先向所有核心广播 RFO 请求,将其它核心的 Cache Line 置为 “已失效”。
- 其它核心在读取或写入 “已失效” 数据时,需要先将其它核心 “已修改” 的数据写回内存,再从内存读取;
- 情况 2 - 修改不同变量:
- 两个处理器并行修改不同变量的情况,从程序员的逻辑上看,两个核心没有数据依赖关系,因此每次写入操作并不需要把其他核心的 Cache Line 置为 “已失效”。
- 但从 CPU 的缓存一致性机制上看,由于 CPU 缓存的颗粒度是一个个缓存行,而不是其中的一个个变量。当修改其中的一个变量后,缓存控制机制也必须把其它核心的整个 Cache Line 置为 “已失效”。
- 那么什么是伪共享问题
- 多个核心修改同一个变量时,使用 MESI 机制维护数据一致性是必要且合理的。但是多个核心分别访问不同变量时,MESI 机制却会出现不符合预期的性能问题。
- 在高并发的场景下,核心的写入操作就会交替地把其它核心的 Cache Line 置为失效,强制对方刷新缓存数据,导致缓存行失去作用,甚至性能比串行计算还要低。
8.2 如何解决伪共享
- 那么,怎么解决伪共享问题呢?其实方法很简单 —— 缓存行填充:
- 1、分组: 首先需要考虑哪些变量是独立变化的,哪些变量是协同变化的。协同变化的变量放在一组,而无关的变量分到不同组;
- 2、填充: 在变量前后填充额外的占位变量,避免变量和其他分组的被填充到同一个缓存行中,从而规避伪共享问题。
8.3 并发集合解决伪共享
更多内容推荐
- GitHub:https://github.com/yangchong211
- 博客:https://juejin.cn/user/1978776659695784
- 博客汇总:https://github.com/yangchong211/YCBlogs
- 设计模式专栏:https://github.com/yangchong211/YCDesignBlog
- Java高级进阶专栏:https://github.com/yangchong211/YCJavaBlog
- 网络协议专栏:https://github.com/yangchong211/YCNetwork
- 计算机基础原理专栏:https://github.com/yangchong211/YCComputerBlog
- 系统性学习C编程:https://github.com/yangchong211/YCStudyC
- C++学习案例:https://github.com/yangchong211/YCStudyCpp
- Leetcode算法专栏:https://github.com/yangchong211/YCLeetcode
- Android技术专栏:https://github.com/yangchong211/YCAndroidBlog