中央处理器CPU
- RAM + 寄存器 + ALU 做个CPU
- 解释“指令 -> 解释 -> 执行”这个循环
- 时钟是什么,时钟速度和赫兹
- 超频提升性能,降频省电
重点:
- 拼个CPU出来。
- CPU怎么执行命令?
ALU
:
作用:输出二进制,它会执行计算。
两种内存:
寄存器: 很小的一块内存,能存一个值。RAM
: 是一大块内存,能在不同地址存大量数字。 (寄存器增大后改造成RAM
)
把RAM
, 寄存器
, ALU
放在一起,组件计算机的心脏CPU
(中央处理单元)。
拼个CPU
CPU: 四个寄存器 + 2个寄存器(指令寄存器和指令地址寄存器) + RAM + ALU(指令需要其功能才使用) + “时钟”
CPU
作用: 负责执行程序。
程序由一个个操作组成,这些操作叫做“指令”,因为它们“指示”计算机要做什么。如果是数学指令,比如加/减,CPU
会让ALU
进行数学计算。也可能是内存指令,CPU
会和内存通信,然后读/写值。CPU
里有很多组件。重点放在功能,而不是一根根线具体怎么连。当用一根线连接两个组件时,这条线只是所有必须线路的一个抽象。这种高层次视角叫“微体系架构”。
首先要内存,使用RAM
,为了保持简单,假设它只有16个位置,每个位置存8位。再来四个8位存储器,叫A, B, C, D。寄存器用来 临时存数据 和 操作数据。
数据是以二进制存在内存里。
程序也可以存在内存里。
可以给CPU
支持的所有指令,分配一个ID:
在这个假设的例子,用前四位存“操作代码”(operation code),简称“操作码”(opcode),后四位代表数据来自哪里,可以是寄存器或内存地址。
还需要两个寄存器,来完成CPU
。
- 一个寄存器追踪程序运行到哪里了,叫它“指令地址寄存器”。(存当前指令的内存地址)
- 另外一个寄存器存当前指令, 叫“指令寄存器”。
当启动计算机时,所有寄存器从0开始。
CPU的第一个阶段叫“取指令阶段”(FETCH PHASE),负责拿到指令。
首先,将“指令地址寄存器”连到RAM
,寄存器的值为0,因此RAM
返回地址0的值,0010 1110
会复制到“指令寄存器”里。
现在指令拿到了,要弄清楚是什么指令,才能执行(execute),而不是杀死(kill)它,这是“解码阶段”。
前4位0010
是LODA A
指令,意思是,把RAM
的值放入寄存器A。
后4位1110
是RAM
的地址,转成十进制是14。
接下来,指令由“控制单元”进行解码,就像之前的所有东西,“控制单元”也是逻辑门组成的。
比如:为了识别“LOAD A”指令,需要一个电路,检查操作码是不是0010
,可以用很少的逻辑门来实现。
指令:
知道了什么是指令,就可以开始执行了,开始“执行阶段”。
- 用“检查是否LOAD_A指令的电路”,可以打开
RAM
的“允许读取线”,把地址14传过去,RAM
拿到值,0000 0011
,十进制的3。 - 因为是
LOAD_A
指令,想把这个值只放到寄存器A,其它寄存器不受影响,所以需要一根线,把RAM
连接到4个寄存器,用“检查是否LOAD_A指令的电路”启用寄存器A的“允许写入线”。
成功把RAM
地址14的值,是十进制的3,放到了寄存器A:
CPU怎么执行命令?
“取指令 -> 解码 -> 执行”
指令完成了,可以关掉所有线路:
去拿下一条指令,“指令地址寄存器”+1,“执行阶段”结束。
LOAD_A
只是CPU可以执行的各种指令之一,不同指令由不同逻辑电路解码,这些逻辑电路会配置CPU内的组件来执行对应操作。
现在计算机支持更多指令,直接操作内存,但原理上是如此。
复杂的逻辑电路解码,把它封装成一个整体的“控制单元”。
汇编指令与机器指令就是一一对应。
控制单元就像管弦乐队的指挥,“指挥”CPU的所有组件。“取指令 -> 解码 -> 执行”完成。
后续可以再从“取指令”开始:
- “指令地址寄存器”现在值是1,所以
RAM
返回地址1里的值:0001 1111
。 - 到了“解码”阶段,
0001
是LOAD_B
指令,从RAM
里把一个值复制到寄存器B,这次内存地址是1111
,十进制的15。 - 现在到“执行阶段”,“控制单元”叫
RAM
读地址15,并配置寄存器B接受数据。 - 完成,把值
0001 1110
也就是十进制的14存到了寄存器B。 - 最后,再把“指令地址寄存器”+1。
- “执行阶段”结束。
后续可以再从“取指令”开始:
- “指令地址寄存器”现在值是2,所以
RAM
返回地址1里的值:1000 0100
。 - 1000是ADD指令,这次后面4位不是
RAM
地址,而是2位,2位,分别代表2个寄存器,2位可以表示4个值(00, 10, 01, 11),所以足够表示4个寄存器。 - 第一个地址是01,代表寄存器B,第二个地址是00,代表寄存器A。因此,
1000 0100
,代表把寄存器B,加到寄存器A里。 - 为了执行这个指令,要整合ALU。“控制单元”负责选择正确的寄存器作为输入,并配置ALU执行正确的操作。
- 对于
ADD
指令,“控制单元”会,启用寄存器B,作为ALU的第一个输入,还启用寄存器A,作为ALU的第二个输入。ALU可以执行不同操作,所以控制单元必须传递ADD操作码告知ALU要做什么。 - 最后,结果应该存到寄存器A。但不能直接写入寄存器A,这样新值会进入ALU,不断和自己相加。因此,控制单元用自己的一个寄存器暂时保存结果,关闭ALU,然后把值写入正确的寄存器。这里3 + 14 = 17,二进制是
0001 0001
。 - 最后,再把“指令地址寄存器”+1。
-
ADD
结束。
后续可以再从“取指令”开始:
- “指令地址寄存器”现在值是3,所以
RAM
返回地址3里的值:0100 1101
。 - 解码得知是
STORE A
指令(把寄存器A的值放入到内存)RAM
地址13中。 - 接下来,把地址传给
RAM
,但这次不是“允许读取”而是“允许写入”;同时,打开寄存器A的“允许读取”,这样就可以把寄存器A里的值,传给RAM
。
运行第一个电脑程序:
它从内存中加载两个值,相加,然后把它结果放回到内存中。
人工切换CPU的状态:“取指令 -> 解码 -> 执行”。
最后:STORE_A 13
指令执行完之后,再执行HALT
指令。否则CPU会不停运行下去,处理后面的0。因为0不是操作码,所以电脑会崩掉。
一般来说RAM
没写的话,里面会储存乱码而不是0(不HALT
就会崩掉)。
时钟
是“时钟”来负责管理CPU的节奏,时钟以精确的间隔,触发电信号,“控制单元”会用这个信号,推进CPU的内部操作。就像罗马帆船的船头,有一个人负责按节奏的击鼓,让所有划船的人同步。就像节拍器一样。
节奏不能太快,因为就算是电也要一定时间来传输,CPU“取指令 -> 解码 -> 执行”的速度叫“时钟速度”。单位是赫兹,赫兹是用来表示频率的单位。1赫兹代表一秒1个周期。
前面的步骤:读取 -> 读取 -> 相加 -> 存储
时钟速度大概是0.03赫兹,(1/360 = 0.03Hz)。
第一个单芯片CPU是“英特尔 4004”1971年发布的4位CPU。
它的微架构:
虽然是第一个单芯片的处理器,但它的时钟速度达到了740千赫兹 - 每秒74万次。
一兆赫兹是1秒1百万个时钟周期,现在的电脑或手机,肯定有几千兆赫兹。1秒10亿次时钟周期。
计算机超频,意思是修改时钟速度,加快CPU的速度。芯片制造商经常会给CPU留一点余地,可以接受一点超频。但超频太多会让CPU过热,或产生乱码,因为信号跟不上时钟(超频一般不会让计算机起火)。
降频,有时候没有必要让处理器全速运行,可能用户走开了,或者在跑一个性能要求较低的程序,把CPU的速度降下来,可以省很多电。省电对用户电池的设备很重要,比如笔记本和手机。
为了尽可能省电,很多现代处理器可以按需求,加快或减慢时钟速度。这叫“动态调整频率”。加上时钟后,CPU才是完整的。
把寄存器
和ALU
和时钟
封装在一起:
RAM
,是在CPU
外边的独立组件。CPU
和RAM
之间用“地址线”,“数据线”和“允许读/写线”进行通信。
指令和程序
重点:运行一遍程序。
- 介绍“指令集”。
LOAD_A
,LOAD_B
,SUB
,JUMP
,ADD
,HALT
等指令。 - 带条件跳转,
JUMP NEGATIVE
是负数才跳转,还有其他类型的JUMP
。 - 真正现在CPU用更多指令集。位数更长。
- 1971年的英特尔4004处理器,有46个指令。
- 如今英特尔酷睿i7,有上千条指令。
把ALU
, 控制单元, RAM
, 时钟结合在一起,做了一个基本,但可用的“中央处理单元”,简称CPU。它是计算机的核心。CPU之所以强大,是因为它是可编程的,如果写入不同指令,就会执行不同任务。CPU是一块硬件,可以被软件控制。
介绍指令集
-
LOAD_A
寄存器A -
LOAD_B
寄存器B -
ADD
相加。 -
STORE_A
存储寄存器A,并写入RAM
中。 -
SUB
减法,和ADD
一样也要2个寄存器来操作。 -
JUMP(跳转)
,让程序跳转到新位置。如果想改变指令顺序,或跳过一些指令,这个就很实用。例如:JUMP 0
,可以调回开头。JUMP
在底层的实现方式是:把指令后4位代表的内存地址的值覆盖掉“指令地址寄存器”里的值。 -
JUMP_NEGATIVE
,它只在ALU的“负数标志”为真时,进行JUMP
。算数结果为负,“负数标志”才是真,结果不是负数时,“负数标志”为假,如果是假,JUMP_NEGATIVE
就不会执行程序照常运行。 -
HALT(停止)
,计算机还需要知道什么时候该停下来。
条件跳转
指令和数据都是存在同一个内存里的。它们在根本层面上毫无区别,都是二进制数。 HALT
很重要,能区分指令和数据。
JUMP
让程序更完整一些:
现在从CPU的视角走一遍程序。
- 首先
LOAD_A 14
,把1存入寄存器A(地址14里值是1)。 - 然后
LOAD_B 15
,把1存入寄存器B(地址15里值是1)。 - 然后
ADD B A
把寄存器B和A相加,结果放到寄存器A里。现在寄存器A的值是2,(以二进制存储的) - 然后
STORE_A 13
指令,把寄存器A的值存入内存地址13。 -
JUMP 2
指令,CPU会把“指令寄存器”的值,现在的“4”,改成“2”。因此下一步不是HALT
。 - 再读内存地址2里的指令,也就是
ADD B A
,寄存器A里的2,寄存器B里的1。1+2=3,寄存器A变成3。存入内存。又碰到JUMP 2
,回到ADD B A
,1+3=4。每次循环都+1
,不断增多。如果按照这样执行下去,永远不会碰到HALT
,总是会碰到JUMP 2
。这个叫无限循环(INFINITE LOOP)∞。
JUMP 2
指令:
为了停止下来,需要有条件的JUMP
,只有特定条件满足了,才执行JUMP
。
比如:JUMP NEGATIVE
就是条件跳转的一个例子。
还有其它类型的条件跳转,比如,JUMP IF EQUAL(如果相等)
,JUMP IF GREATER(如果更大)
。
for
循环是有个数限制,while和python里的repeat是无限循环。
执行下图程序:
- 程序先把内存值放入寄存器A和B。寄存器A是11,寄存器B是5。
-
SUB B A
,用A减B,11-5=6。现在寄存器A的值是6 -
JUMP_NEG 5
,ALU上次计算的是6,是正数,所以“负数标志”是假。因此处理器不会执行JUMP
,继续下一条指令。 -
JUMP 2
,JUMP 2
没有条件,直接执行SUB B A
, 6-5=1。现在寄存器A的值是1。 -
JUMP_NEG 5
,因为1还是正数,因此JUMP NEGATIVE
不会执行,来到下一条指令。 -
JUMP 2
,直接执行SUB B A
, 1-5=-4。现在寄存器A的值是-4。这次ALU
的“负数标志”是真。现在寄存器A的值是-4。 -
JUMP_NEG 5
, CPU的执行跳转到内存地址5的指令ADD B A
。跳出了无限循环。 -
ADD B A
,-4+5=1,存入寄存器A,现在寄存器A的值是1。 -
STORE_A 13
,存入内存地址13 -
HALT
指令,停止程序。
指令顺序:
-
LOAD_A 14
值是11 -
LOAD_B 15
值是5 -
SUB B A
寄存器A值6(11-5) -
JUMP_NEG 5
"负数标志"false -
JUMP 2
跳转会地址2再次依次执行 -
SUB B A
寄存器A值1(6-5) -
JUMP_NEG 5
"负数标志"false -
JUMP 2
跳转会地址2再次依次执行 -
SUB B A
寄存器A值-4(1-5) -
JUMP_NEG 5
"负数标志"true -
ADD B A
寄存器A值1(-4+5) -
STORE_A 13
存入内存地址13 -
HALT
指令,程序结束
这段程序只有7个指令,但CPU执行了13个指令。因为在内部循环了2次。
这段代码是算余数,11除5余1 -> 11-5-5-5+5 = 1
软件可以做到硬件做不到的事。ALU
可没有除法功能,是程序给了除法这个功能。
假设的CPU很基础,所有指令都是8位,操作码只占了前4位,即便用尽4位,也只能代表16个指令。而且有几条指令,是用后4位来指定内存地址。只能表示16个指令,并不多,甚至不用使用JUMP 17
,因为4位二进制无法表示数字17,因此,真正的现代CPU用两种策略。
- 最直接的方法是用更多位来代表指令,比如32位或64位。这叫 “指令长度”
- “可变指令长度”,例如:某个CPU用8位长度的操作码,如果看到
HALT
指令,HALT
不需要额外数据,那么会马上执行。如果看到JUMP
,它得知道位置值,这个值在JUMP
的后边。这个叫“立即值”,这样设计,指令可以是任意长度。但会让读取阶段复杂一些。
某些指令不需要操作内存所以可以省下内存那四位。
但是JUMP
也需要4位操作码,这样还是只有4位来表示内存地址。
一般来说 (指令=操作码+操作值地址)。当(指令=操作码+操作值)时,这个操作值就是立即值。
这样大于8位的地址,可以通过多次重复读取的方式获得,这也是读取阶段相对麻烦的地方。
立即值是指令+位置
英特尔4004处理器
1971年,英特尔发布了4004处理器。这是第一次把CPU做成一个芯片,给后来的英特尔处理器打下基础。
- 支持46个指令。足够做一台能够用的电脑。
- 使用了前面使用过的指令。比如
JUMP
,ADD
,SUB
,LOAD
- 也用8位的“立即值”来执行
JUMP
,以表示更多内存地址。
处理器从1971年到现在发展巨大,现代CPU,比如英特尔酷睿i7,有上千个指令和指令变种。长度从1到15个字节。
例如:光ADD
指令就很多变种。
指令越来越多,是因为给CPU设计了越来越多功能。
高级CPU设计
- 早期是加快晶体管切换速度,来提升CPU速度。
- 给CPU专门的除法电路+其它电路来做复杂操作,比如游戏,视频解码。
- 给CPU加缓存,提高数据存取速度。
- 脏位(Dirty bit)
- 流水线设计
- 并行处理(parallelize)
- 乱序执行(out-of-order execuition)
- 推测执行(speculative execution)
- 分支预测(branch prediction)
- 多个ALU
- 多核(Code)
- 多个独立CPU
- 超级计算机,中国的“神威 太湖之光”
从1秒1次运算,到现在千兆赫甚至兆赫的CPU。现在看视频的设备也有GHz
速度。
1秒10亿条指令,这是很大的计算量。
减少晶体管切换时间
早期计算机的提速方式是,减少晶体管的切换时间。
晶体管组成了逻辑门,ALU
以及其它计算机组件。
这种提速方法最终会碰到瓶颈,所以处理器厂商,发明了各种新技术来提升性能,不但让简单指令运行更快,也让它能进行更复杂的运算。
除法的程序,给CPU执行,方法是做一连串减法。比如16除4会变成。16-4-4-4-4
,碰到0或负数才停下来。这种方法要多个时钟周期,很低效。所以现代CPU直接在硬件层面设计了除法,可以直接给ALU除法指令。
除法电路
拥有除法的ALU更大也更复杂一些。但也更厉害,复杂度vs速度 的平衡在计算机发展史上经常出现。
例如:现代处理器有专门电路来处理图形操作,解码压缩视频,加密文档等等。如果用标准操作来实现,要很多个时钟周期。可能听说过处理器MMX
, 3DNOW
, SEE
。它们有额外电路做更复杂的操作。用于游戏和加密等场景。
指令不断增加,一旦习惯了它的便利就很难删掉,所以为了兼容旧指令集,指令数量越来越多。
英特尔4004,第一个集成CPU,有46条指令,足够做一台能用的计算机。但现代处理器有上千条指令,有各种巧妙复杂的电路。
超高的时钟速度带来另外一个问题: 如何快速传递数据给CPU。就像有强大的蒸汽机,但无法快速加煤。RAM
成了瓶颈。
RAM
是CPU之外的独立组件,意味着数据要用线来传递,叫“总线”。
总线可能只有几厘米,别忘了电信号的传输接近光速。但CPU每秒可以处理上亿条指令。很小的延迟也会造成问题。
给CPU加缓存
RAM
还需要时间找地址,取数据,配置,输出数据。一条“从内存读数据”的指令可能要多个时钟周期,CPU空等数据。解决延迟的方法之一是:给CPU加一点RAM,叫“缓存”。
因为处理器里空间不大,所以缓存一般只有KB
或MB
,而RAM
都是GB
起步。
缓存提高了速度,CPU从RAM
拿数据时,RAM
不用传一个,可以传一批。虽然花的时间久一点,但数据可以存在缓存。
这很实用,因为数据常常是一个个按顺序处理。
例如:算餐厅的当日收入。先取RAM
地址100的交易额,RAM
与其只给1个值,直接给一批值。把地址100到200都复制到缓存,当处理器要下一个交易额时,地址101,缓存会说:“我有下一个交易额的值”,而不用向RAM
取数据。
因为缓存离CPU近,一个时钟周期就能给数据,CPU不用空等。
比反复去RAM
拿数据快得多,如果想要的数据已经在缓存,叫“缓存命中”。如果想要的数据不在缓存,叫“缓存未命中”。
缓存也可以当临时空间,存一些中间值,适合长/复杂的运算。
如果计算完餐厅一天销售额,想把结果存到地址150,就像之前,数据不是直接存到RAM
,而是存在缓存,这样不但存起来快一些,如果还要接着算,取值也快一些。
但这样带来一个问题,缓存和RAM
不一致了,这种不一致必须记录下来,之后要同步。因此缓存里每块空间,有一个特殊标记,叫“脏位(Dirty bit)”。
同步一般发生在,当缓存满了而CPU又要缓存时,在清理缓存腾出空间之前,会先检查“脏位”,如果是“脏”的,在加载新内容之前,会把数据写会到RAM
。
流水线设计
提升性能方法叫“指令流水线”。
例如:洗一整个酒店的床单,但只有1个洗衣机, 1个干燥机。
选择1: 按顺序来,放洗衣机等30分钟洗完,然后拿出湿床单,放进干燥机等30分钟烘干。这样1小时洗一批。
需要用“并行处理”进一步提高效率,先放一批床单到洗衣机,等30分钟洗完,然后湿床单放进干燥机,但这次,与其干等30分钟烘干,可以放另一批进洗衣机。让两台机器同时工作,30分钟后,一批床单完成,另一批完成一半,另一批准备开始,效率是翻倍的。
处理器也可以这样子设计,CPU是按序处理,取指令(fetch) -> 解码(decode) -> 执行(execute)
,不断重复。这种设计,三个时钟周期执行1条指令。
并行处理
但因为每个阶段用的是CPU的不同部分,意味着可以并行处理,“执行”一个指令时,同时可以“解码”下一个指令,“读取”下下个指令。不同任务重叠进行,同时用上CPU里所有部分。这样的流水线,每个时钟周期执行1个指令,吞吐量*3。
和缓存一样,这也会带来一些问题:
- 指令之间的依赖关系。例如:在读某个数据,而正在执行的指令会改这个数据。也就是说拿是旧数据,因此流水线处理器,要先弄清数据依赖性,必要时停止流水线,避免出问题。
高端CPU,比如笔记本和手机里的,会更进一步,动态排序 有依赖关系的指令。 最小化流水线的停工时间。这叫“乱序执行(out-of-order execuition)”。
这种电路非常复杂,但因为非常高效,几乎所有现代处理器都有流水线。 - “条件跳转”,比如“JUMP NEGATIVE”,这些指令会改变程序的执行流,简单的流水线处理器,看到
JUMP
指令会停一会儿等待条件值确定下来,一旦JUMP
的结果出来了,处理器就继续流水线。但因为空等会造成延迟,所以高端处理器会用一些技巧(调度算法,冒险分支)。
可以把JUMP
想象成是“岔路口”,高端CPU会猜哪条路的可能性大一些,然后提前把指令放进流水线,这叫“推测执行(speculative execution)”。当JUMP
的结果出了,如果CPU
猜对了,流水线已经塞满正确可以执行的指令,可以马上运行。如果CPU
猜错了,就要清空流水线,就像走错路掉头。
为了尽可能减少清空流水线的次数,CPU厂商开发了复杂的方法,来猜测哪条分支更有可能,叫做“分支预测(branch prediction)”。现代CPU的正确率超过90%。
多个ALU
理想情况下,流水线一个时钟周期完成1个指令,然后“超标量处理器”出现了,一个时钟周期完成多个指令。即便有“流水线设计”,在指令执行阶段,处理器里有些区域还是可能会空闲,比如,执行一个“从内存取值”指令期间ALU
会闲置。所以一次性处理多条指令(取指令+解码)会更好。
可以再进一步,加多几个相同的电路,执行出现频次很高的指令。(一核有难,多核围观)
例如:很多CPU有四个,八个甚至更多完全相同的ALU
。可以同时执行多个数学运算。
目前说过的方法:“缓存”,“指令流水线”,“多个ALU”,都是优化1个指令流的吞吐量。
多核(Code)
另外一个提升性能的方法是:同时运行多个指令流,用多核处理器。(七核看戏)
双核或四核处理器,意思是一个CPU芯片里,有多个独立处理单元。很像是有多个独立CPU,但因为它们整合紧密,可以共享一些资源。比如缓存,使得多核可以合作运算。但多核不够时,可以用多个CPU。
高端计算机,视频观看的计算机,需要更多的马力,让上百人以上能够同时流畅的观看,2个或4个CPU是最常见的,但有时有更高的性能需求,所以造了超级计算机。
比如模拟宇宙的形成,需要强大的计算能力。给普通的台式机加几个CPU没什么用,需要更多的处理器。目前为止,最快的计算机在中国无锡的“神威 太湖之光”,有40960个CPU,每个CPU有256和核心,总共超过1千万个核心,每个核心的频率是1.45GHz,每秒可以进行9.3亿亿次浮点运算。
现代的处理器不但大大的提高了速度,而且也变得更复杂,用各种技巧,榨干每个时钟周期,做尽可能多的运算。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。