硬件结构
CPU是如何工作的
图灵机工作方式
图灵机主要由两部分组成:
- 纸带:由格子组成,可写入字符,格子类比内存,字符类比数据;
- 读写头:分为存储单元、控制单元、运算单元
- 存储单元:用于存放数据
- 控制单元:用于识别字符功用
- 运算单元:用于执行运算指令
例子:3=1+2
纸带上为12+,读写头依次读取,识别到数字存储,识别到运算符运算,最后数据为123
冯诺依曼模型
- 运算器
- 控制器
- 存储器
- 输入设备
- 输出设备
组成如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zkHlNMot-1690708501986)(https://pillow-blog-pictures.oss-cn-shanghai.aliyuncs.com/Von_Neumann_architecture.svg)]
内存Memory
中央处理器Control Unit and Arithmetic Logic Unit
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-h6zqpCn9-1690708501987)(https://pillow-blog-pictures.oss-cn-shanghai.aliyuncs.com/%E5%86%AF%E8%AF%BA%E4%BE%9D%E6%9B%BC%E6%A8%A1%E5%9E%8B.png)]
32位CPU单位计算量为4字节,64位CPU单位计算量为8字节,大数据运算,单位计算量大的CPU计算更快。
组成:
- 寄存器:
- 通用寄存器:用来存放需要进行运算的数据,比如需要进行加和运算的两个数据。
- 程序计数器:用来存储 CPU 要执行下一条指令「所在的内存地址」,注意不是存储了下一条要执行的指令,此时指令还在内存中,程序计数器只是存储了下一条指令「的地址」。
- 指令寄存器:用来存放当前正在执行的指令,也就是指令本身,指令被执行完成之前,指令都存储在这里。
- 控制单元:负责控制 CPU 工作
- 逻辑运算单元:负责数据计算
总线
总线用于 CPU 和内存以及其他设备之间的通信,告知CPU数据地址,数据内容,是读是写
- 地址总线,用于指定 CPU 将要操作的内存地址;
- 数据总线,用于读写的内存数据;
- 控制总线,用于发送和接收信号,比如中断、设备复位等信号,CPU 收到信号后自然进行响应,这时也需要控制总线;
输入、输出设备
输入设备向计算机输入数据,计算机经过计算后,把数据输出给输出设备
线路位宽和CPU位宽
线路传输数据通过操作电压实现,低电压0,高电压1
若是只有两条线路,传输0110需要两次才可以完成,所以线路的位宽最好一次就能访问到所有内存地址。
CPU 想要操作「内存地址」就需要「地址总线」:
- 如果地址总线只有 1 条,那每次只能表示 「0 或 1」这两种地址,所以 CPU 能操作的内存地址最大数量为 2(2^1)个(注意,不要理解成同时能操作 2 个内存地址);
- 如果地址总线有 2 条,那么能表示 00、01、10、11 这四种地址,所以 CPU 能操作的内存地址最大数量为 4(2^2)个。
32 位 CPU 一次最多只能操作 32 位宽的地址总线和数据总线
如果计算的数额不超过 32 位数字的情况下,32 位和 64 位 CPU 之间没什么区别的,只有当计算超过 32 位数字的情况下,64 位的优势才能体现出来。
程序执行基本过程
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uY2en85Q-1690708501987)(https://pillow-blog-pictures.oss-cn-shanghai.aliyuncs.com/CPU%E6%89%A7%E8%A1%8C%E7%A8%8B%E5%BA%8F.png)]
最后能够回忆到以下内容
CPU 执行程序的过程如下:
- 第一步,CPU 读取「程序计数器」的值,这个值是指令的内存地址,然后 CPU 的「控制单元」操作「地址总线」指定需要访问的内存地址,接着通知内存设备准备数据,数据准备好后通过「数据总线」将指令数据传给 CPU,CPU 收到内存传来的数据后,将这个指令数据存入到「指令寄存器」。
- 第二步,「程序计数器」的值自增,表示指向下一条指令。这个自增的大小,由 CPU 的位宽决定,比如 32 位的 CPU,指令是 4 个字节,需要 4 个内存地址存放,因此「程序计数器」的值会自增 4;
- 第三步,CPU 分析「指令寄存器」中的指令,确定指令的类型和参数,如果是计算类型的指令,就把指令交给「逻辑运算单元」运算;如果是存储类型的指令,则交由「控制单元」执行;
简单总结一下就是,一个程序执行的时候,CPU 会根据程序计数器里的内存地址,从内存里面把需要执行的指令读取到指令寄存器里面执行,然后根据指令长度自增,开始顺序读取下一条指令。
CPU 从程序计数器读取指令、到执行、再到下一条指令,这个过程会不断循环,直到程序执行结束,这个不断循环的过程被称为 CPU 的指令周期。
32位执行a=1+2
简要过程:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ENftNevh-1690708501988)(https://pillow-blog-pictures.oss-cn-shanghai.aliyuncs.com/%E6%95%B0%E6%8D%AE%E6%AE%B5%E4%B8%8E%E6%AD%A3%E6%96%87%E6%AE%B5.png)]
编译器会把 a = 1 + 2
翻译成 4 条指令,存放到正文段中。如图,这 4 条指令被存放到了 0x100 ~ 0x10c 的区域中:
- 0x100 的内容是
load
指令将 0x200 地址中的数据 1 装入到寄存器R0
; - 0x104 的内容是
load
指令将 0x204 地址中的数据 2 装入到寄存器R1
; - 0x108 的内容是
add
指令将寄存器R0
和R1
的数据相加,并把结果存放到寄存器R2
; - 0x10c 的内容是
store
指令将寄存器R2
中的数据存回数据段中的 0x208 地址中,这个地址也就是变量a
内存中的地址;
编译完成后,具体执行程序的时候,程序计数器会被设置为 0x100 地址,然后依次执行这 4 条指令。
指令
MIPS指令集
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-l2iDJzlW-1690708501988)(https://pillow-blog-pictures.oss-cn-shanghai.aliyuncs.com/MIPS%E6%8C%87%E4%BB%A4%E9%9B%86.png)]
R指令用于算数和逻辑操作,I指令用于数据传输、条件分支等,J指令用于跳转
具体比如:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hjXlUulG-1690708501988)(https://pillow-blog-pictures.oss-cn-shanghai.aliyuncs.com/add%E7%9A%84MIPS%E6%8C%87%E4%BB%A4.png)]
加和运算 add 指令是属于 R 指令类型:
- add 对应的 MIPS 指令里操作码是
000000
,以及最末尾的功能码是100000
,这些数值都是固定的,查一下 MIPS 指令集的手册就能知道的; - rs 代表第一个寄存器 R0 的编号,即
00000
; - rt 代表第二个寄存器 R1 的编号,即
00001
; - rd 代表目标的临时寄存器 R2 的编号,即
00010
; - 因为不是位移操作,所以位移量是
00000
把上面这些数字拼在一起就是一条 32 位的 MIPS 加法指令了,那么用 16 进制表示的机器码则是 0x00011020
。
指令周期
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hrdelfGo-1690708501988)(https://pillow-blog-pictures.oss-cn-shanghai.aliyuncs.com/CPU%E6%8C%87%E4%BB%A4%E5%91%A8%E6%9C%9F.png)]
- CPU 通过程序计数器读取对应内存地址的指令,这个部分称为 Fetch(取得指令);
- CPU 对指令进行解码,这个部分称为 Decode(指令译码);
- CPU 执行指令,这个部分称为 Execution(执行指令);
- CPU 将计算结果存回寄存器或者将寄存器的值存入内存,这个部分称为 Store(数据回写);
指令类型
指令从功能角度划分,可以分为 5 大类:
- 数据传输类型的指令,比如
store/load
是寄存器与内存间数据传输的指令,mov
是将一个内存地址的数据移动到另一个内存地址的指令; - 运算类型的指令,比如加减乘除、位运算、比较大小等等,它们最多只能处理两个寄存器中的数据;
- 跳转类型的指令,通过修改程序计数器的值来达到跳转执行指令的过程,比如编程中常见的
if-else
、switch-case
、函数调用等。 - 信号类型的指令,比如发生中断的指令
trap
; - 闲置类型的指令,比如指令
nop
,执行后 CPU 会空转一个周期;
磁盘比内存慢几万倍
磁盘内存都同属存储器,计算机的存储器还有:
- CPU寄存器
- CPU Cache高速缓存
- 内存
- SSD/HDD硬盘
时钟周期
即CPU时钟周期,这与CPU的主频有关,比如我的12490f单核频率为4.6GHz,那么它的时钟周期大概为:1/4.6约等于0.22ns
各个存储器的读写速度
寄存器
半个时钟周期
CPU Cache
使用SRAM-Static Random Access Memory-静态随机存储器,1bit数据需要6个晶体管,所以断电后数据就会消失。
CPU的高速缓存分为L1、L2、L3三层,如果你使用的window系统,可以从任务管理器中查看到自己的三级缓存大小。
L1 高速缓存的访问速度几乎和寄存器一样快,通常只需要 2~4
个时钟周期,L1 高速缓存通常分成指令缓存和数据缓存。
L2 高速缓存每个CPU核心都会有,访问速度在 10~20
个时钟周期。
L3 高速缓存通常是多个 CPU 核心共用的,访问速度在 20~60
个时钟周期。
内存
使用DRAM-Dynamic Random Access Memory-动态随机存取存储器,1bit数据需要1个晶体管和1个电容,电容的存在让数据能够存储,但需要定时刷新电容才会保证数据不会丢失。访问速度在200~300
个 时钟周期之间。
SSD/HDD硬盘
SSD也就是固态硬盘,材料结构与内存类似,访问速度比内存慢10~1000倍
。
HDD机械硬盘,远古产物,物理读写,速度比内存慢10w
左右。
存储器的层次关系
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YsHBj72X-1690708501988)(https://pillow-blog-pictures.oss-cn-shanghai.aliyuncs.com/%E5%AD%98%E5%82%A8%E5%99%A8%E7%9A%84%E5%B1%82%E6%AC%A1%E5%85%B3%E7%B3%BB%E5%9B%BE.png)]
如何写出让CPU跑的更快的代码
明白了计算机存储设备的访问速度,只要使得数据更容易在高速缓存区找到,而不是去龟速的硬盘去找即可提高代码速度。我们需要知道Cache的数据存储机制。
CPU Cache结构
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BhGuXWW0-1690708501989)(https://pillow-blog-pictures.oss-cn-shanghai.aliyuncs.com/Cache%E7%9A%84%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84.png)]
Cache Line 即 L1 Cache一次载入数据的大小。
Cache的数据访问逻辑
直接映射Cache-Direct Mapped Cache
即直接把内存块地址始终映射到一个CPU Cache Line地址,映射关系为取模运算,同时为避免多个内存块对应同一个CPU Cache Line地址,在对应的 CPU Cache Line 中我们还会存储一个组标记(Tag),用于区分不同的内存块。
除此之外,CPU Cache Line还有两个信息:数据(Data)和有效位(Valid bit)
CPU从Cache读取数据会只寻找需要的一个数据片段,因此还会需要一个偏移量(Offset)
一个内存的访问地址,包括组标记、CPU Cache Line 索引、偏移量这三种信息,于是 CPU 就能通过这些信息,在 CPU Cache 中找到缓存的数据。而对于 CPU Cache 里的数据结构,则是由索引 + 有效位 + 组标记 + 数据块组成
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lFBIGi0j-1690708501989)(https://pillow-blog-pictures.oss-cn-shanghai.aliyuncs.com/%E7%9B%B4%E6%8E%A5Cache%E6%98%A0%E5%B0%84.png)]
CPU的缓存一致性
从Cache中读取是要比在内存中读取更加快速,提高代码的缓存命中率能够有效提高程序的性能。
数据不只有读操作,还有写操作,数据写入Cache后,与内存的数据不一致,此时需要同步操作。
Cache的写入策略
主流的CPU的缓存写入策略为:写直达和写回
写直达
即保持内存与Cache数据一致的最简单的方法——将数据同时写入Cache中
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lRkmalCr-1690708501989)(https://pillow-blog-pictures.oss-cn-shanghai.aliyuncs.com/%E5%86%99%E7%9B%B4%E8%BE%BE.png)]
如此,无论数据在不在Cache里,每次写操作都会将数据写入到内存中,即是内存存在数据?
写回
在写回机制中,当发生写操作时,新的数据仅仅被写入 Cache Block 里,只有当修改过的 Cache Block「被替换」时才需要写到内存中,减少了数据写回内存的频率,这样便可以提高系统的性能。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ERv5yNzH-1690708501989)(https://pillow-blog-pictures.oss-cn-shanghai.aliyuncs.com/%E5%86%99%E5%9B%9E1.png)]
Cache Block是否为脏表示其与内存里的数据是否一致,添加是否为脏的条件判断能够对是否同步Cache与内存数据提供判断依据,减少不必要的同步操作。
缓存一致性问题
CPU的每个核心都拥有独立的Cache,核心与核心之间的数据无法一致,于是出现执行结果错误的问题。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-u3ZpVc4y-1690708501989)(https://pillow-blog-pictures.oss-cn-shanghai.aliyuncs.com/%E7%BC%93%E5%AD%98%E4%B8%80%E8%87%B4%E6%80%A7%E9%97%AE%E9%A2%98%E4%BE%8B%E5%AD%902.png)]
写传播和事物的串行化
要解决缓存一致性问题,保证两点即可:
- 某个CPU核心Cache数据更新时,将更新同步到其他核心的Cache,此称之为写传播;
- 某个CPU核心对于数据的操作顺序信息,传到其他核心是一样的,也就是同步操作的顺序也需要一致,此称之为事物的串行化。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PaWOCakJ-1690708501989)(https://pillow-blog-pictures.oss-cn-shanghai.aliyuncs.com/%E4%BA%8B%E4%BB%B6%E9%A1%BA%E5%BA%8F%E9%97%AE%E9%A2%98-20230716104948646.png)]
做到事物串行化,需要引入锁,如果两个 CPU 核心里有相同数据的 Cache,那么对于这个 Cache 数据的更新,只有拿到了「锁」,才能进行对应的数据更新。
总线嗅探与MESI协议
总线嗅探即在各个CPU核心Cache之间穿起一条总线,当嗅探到数据变化时,将此作为事件传播到其他拥有相同数据的核心Cache进行数据更新。
但是无时不刻进行嗅探(监听),无论其他核心Cache是否有相同数据都需要发出一个事件,此会增加总线负载且无法保证事物串行化,于是出现MESI协议。
MESI 协议即:
- Modified,已修改
- Exclusive,独占
- Shared,共享
- Invalidated,已失效
表示Cache Line的四种不同状态。
我们举个具体的例子来看看这四个状态的转换:
- 当 A 号 CPU 核心从内存读取变量 i 的值,数据被缓存在 A 号 CPU 核心自己的 Cache 里面,此时其他 CPU 核心的 Cache 没有缓存该数据,于是标记 Cache Line 状态为「独占」,此时其 Cache 中的数据与内存是一致的;
- 然后 B 号 CPU 核心也从内存读取了变量 i 的值,此时会发送消息给其他 CPU 核心,由于 A 号 CPU 核心已经缓存了该数据,所以会把数据返回给 B 号 CPU 核心。在这个时候, A 和 B 核心缓存了相同的数据,Cache Line 的状态就会变成「共享」,并且其 Cache 中的数据与内存也是一致的;
- 当 A 号 CPU 核心要修改 Cache 中 i 变量的值,发现数据对应的 Cache Line 的状态是共享状态,则要向所有的其他 CPU 核心广播一个请求,要求先把其他核心的 Cache 中对应的 Cache Line 标记为「无效」状态,然后 A 号 CPU 核心才更新 Cache 里面的数据,同时标记 Cache Line 为「已修改」状态,此时 Cache 中的数据就与内存不一致了。
- 如果 A 号 CPU 核心「继续」修改 Cache 中 i 变量的值,由于此时的 Cache Line 是「已修改」状态,因此不需要给其他 CPU 核心发送消息,直接更新数据即可。
- 如果 A 号 CPU 核心的 Cache 里的 i 变量对应的 Cache Line 要被「替换」,发现 Cache Line 状态是「已修改」状态,就会在替换前先把数据同步到内存。
疑问:3里——则要向所有的其他 CPU 核心广播一个请求,要求先把其他核心的 Cache 中对应的 Cache Line 标记为「无效」状态,然后 A 号 CPU 核心才更新 Cache 里面的数据——标记无效的是所有核心还是所有同为S的核心?
CPU是如何执行任务的
Cache伪共享及解决方法
当多个线程同时读写同一个Cache Line的不同变量时,导致CPU Cache失去应有的作用,这种现象称为伪共享。
出现问题的根本在于多个线程需要的数据在同一个Cache Line中,只需要避免这些数据在同一个Cache Line中即可。
系统层面:
Linux内核使用__cacheline_aligned_in_smp
宏定义解决伪共享问题,多核系统中该宏定义是__cacheline_aligned
,使得变量在Cache Line中是对齐的,即使用空间换取时间。
应用层面:
Java的并发框架Disruptor中,有一个RingBuffer类会经常被多个线程使用
RingBufferPad中的7个long类型数据作为Cache Line的前置填充,RingBuffer中的7个long类型数据作为Cache Line的后置填充,即RingBufferFelds前后各被占了56位
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SFmiT8de-1690708501990)(https://pillow-blog-pictures.oss-cn-shanghai.aliyuncs.com/%E5%A1%AB%E5%85%85%E5%AD%97%E8%8A%82.png)]
所以无论怎么加载Cache Line,整个Cache Line里都没有将会发生更新操作的数据,只要数据被频繁访问,不被淘汰,自然没有数据被换出的可能,也不会产生伪共享的问题(只是占用空间有点多)
CPU如何选择线程
Linux内核里的调度器,调度对象为task_struct
,主要分为两类:
- 实时任务:优先级在0~99,要尽快执行的任务
- 普通任务:优先级在100~139。
Linux给任务分配优先级使用调度类,分为三种:
- Deadline
- Realtime
- Fair
前两种的调度策略有:
- SCHED_DEADLINE:是按照 deadline 进行调度的,距离当前时间点最近的 deadline 的任务会被优先调度;
- SCHED_FIFO:对于相同优先级的任务,按先来先服务的原则,但是优先级更高的任务,可以抢占低优先级的任务,也就是优先级高的可以「插队」;
- SCHED_RR:对于相同优先级的任务,轮流着运行,每个任务都有一定的时间片,当用完时间片的任务会被放到队列尾部,以保证相同优先级任务的公平性,但是高优先级的任务依然可以抢占低优先级的任务;
最后一种调度策略有:
- SCHED_NORMAL:普通任务使用的调度策略;
- SCHED_BATCH:后台任务的调度策略,不和终端进行交互,因此在不影响其他需要交互的任务,可以适当降低它的优先级。
什么是软中断
中断是计算机的一种异步的事件处理机制,可以提高系统的并发能力。
操作系统接收到中断请求会打断其他程序运行,中断处理程序需要尽可能快的执行完,以减少对正常进程运行调度的影响。当有多个中断请求时,会有中断请求丢失的情况,此为硬中断。
中断请求的处理程序应该要短且快,这样才能减少对正常进程运行调度地影响,而且中断处理程序可能会暂时关闭中断,这时如果中断处理程序执行时间过长,可能在还未执行完中断处理程序前,会丢失当前其他设备的中断请求。
Linux将中断的执行过程分为两个阶段:上半部分和下半部分。
上半部分硬中断,暂时关闭中断请求通道,负责处理跟硬件紧密相关或者时间敏感的事情。
下半部分软中断,负责延迟处理上半部分未完成的工作,一般以内核线程方式运行。
0.1+0.2!=0.3?
整型,像是int类型是32位,最高位作为符号标志位,正数为0,负号为1
负数在计算机内以补码表示,即:将正数的二进制全部取反加一
整型 | 符号位 | 二进制 |
---|---|---|
5 | 0 | 00000000000000000000000000000101 |
取反 | ||
1 | 1111111111111111111111111111010 | |
加一 | ||
-5 | 1 | 1111111111111111111111111111011 |
这样-5+5=0
如果负数不是使用补码的方式表示,则在做基本对加减法运算的时候,还需要多出一步操作来判断是否为负数,对人来说很简单,对机器来说就会多出很多步骤。
绝大多数的计算机的浮点型,都采用的是IEEE制定的国际标准
浮点数分为三个部分:
- 符号位:表示数字是正数还是负数;
- 指数位:指定了小数点在数据中的位置;
- 尾数位:小数点右边的数字,即小数部分。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MxU1Ref6-1690708501990)(https://pillow-blog-pictures.oss-cn-shanghai.aliyuncs.com/float%E5%AD%98%E5%82%A8.png)]
0.1 和 0.2 这两个数字用二进制表达会是一个一直循环的二进制数,比如 0.1 的二进制表示为 0.0 0011 0011 0011… (0011 无限循环),对于计算机而言,0.1 无法精确表达,这是浮点数计算造成精度损失的根源。
因此,IEEE 754 标准定义的浮点数只能根据精度舍入,然后用「近似值」来表示该二进制,那么意味着计算机存放的小数可能不是一个真实值。