用了很久的计算机了,但是总说不上计算机是做什么的,在学习完计算机组成原理并设计仿真了一个CPU后,我才发现计算机和CPU做的事就是按照人们设计好的指令序列,不断取指并执行,一直重复这么一个过程直到关机。下面我们就两个简单的过程深入分析。
计算机:主机(cpu + 主存) + 输入输出设备
计算机是由cpu、主存和I/O设备组成的,:主存中存储来自磁盘的指令,cpu执行指令,并与外部设备进行交互。
cpu
cpu基本组成
我们从cpu需要做什么再确定其需要什么部件,cpu只需要一直重复做这两件事:取指 执行
下面是用cpp模拟的cpu的执行过程:将每条指令想象成一个函数被主程序调用,主程序就是一个简单的无限循环,它决定要调用的函数,然后调用该函数,循环往复。函数内部就是各个微指令序列(后面会介绍)
class Interp {
int PC ; // 存放下一条 指令的地址
int AC ; // 累加器:用于算术运算的寄存器
int IR; // 指令寄存器 存放当前指令的寄存器
int instr_type; // 指令类型 (操作码)
int data_loc = -1; // 数据地址 , 默认为-1
int data; // 存放当前操作数
bool run_bit = true; // 运行指示位 , 停机时为false
int get_instr_type(int IR){...}
int find_data(int IR ,int instr_type){...}
void execute(int data){...}
public :
void interpret(int memory[], int start_address) {
PC = start_address;
while(run_bit){
IR = memory[PC ++]; // 将指令取出 加载到 IR 中,然后自增
instr_type = get_instr_type(IR ); // 对 指令 分析得到 操作码类型
data_loc = find_data(IR,instr_type); // 取操作数地址 (寻址)
if(data_loc > 0 )
data = memory[data_loc]; // 访存
execute(data) ; // 执行指令
}
}
};
这三件事逻辑都很好理解,接下来我们可以就发现CPU得由控制单元(CU)、运算单元(ALU)和存储单元(寄存器)三个模块通过内部总线连接构成来完成 “执行”,再将CPU通过总线和内存连接起来就可以实现**“取指”**。这部分构成了一个基本的主机。
指令执行
指令是CPU能直接识别并执行的最小单元,了解指令的组成和执行过程基本上就可以了解整个CPU的执行过程了。
指令组成: 操作码 + 寻址方式 (可能没有)+ 操作数(0,1,2,3 个)
指令分类 :
- 数据移动指令 (mov 本质是copy)
- 双操作数指令 (常见的运算指令)
- 单操作数指令 (移位运算,自增,相反数等等)
- 比较和条件转移指令 (CMP [x] , 1 (JNE L1)…)
- 过程调用指令 (call function)
- 循环控制指令 (条件性转移指令 + 基本指令)
- 输入/输出指令 (中断 ,DMA,程序控制I/O)
一些寻址方式:
-
立即寻址 立即数 就是操作数 比如4
-
直接寻址 :4 变成了地址 men[4] 需要访存 寻找操作数
-
间接寻址 :men[men[4]] 需要访存两次 才能得到
-
变址寻址 :把 124300 当成立即数地址即可加到R2上就是操作数的地址(R2+1234300)
-
基址变址寻址:多用一个寄存器R5来存储基址,比如上一个图我们可以把124300存在R5里面。这样第二个操作数的地址就是: [R2] + [R5]
指令执行的过程实际上是分两种的:
1、硬件直接执行 (一般是RISC)
2、(解释器)将指令分解成更小的微指令序列 去执行。
一条指令 对应一个微程序,微程序取出指令、译码之后执行。
程序 是有序的指令序列 ,每一个指令,对应一个微程序
那微程序是否也和程序很像呢?
是的,微程序是有序的微指令序列构成,每一条微指令就是一个 一连串的硬件控制信号,可直接指导cpu工作。
一条指令对应一个微程序, 每一个微程序由一个或多个微指令构成。
下面是微指令的格式 和 微程序的主存:
微指令 :各个控制信号 + 下一周期微指令的地址。
控制存储器 : 存放微程序 (微程序的 主存)
也有类似指令层用的寄存器 : MPC , MIR , MBR
总结来说 :指令的操作码部分 被译码为ALU 可以直接识别的操作 , 操作数部分用来寻址数据,这些操作需要由微程序来指导硬件执行。每一个操作码都对一个不同的微程序
cpu数据通路
上面我们谈到的都是cpu的执行一条指令的过程,那这些硬件之间到底是怎么连接,按照什么逻辑运行的呢?
数据通路是由ALU及其输入和输出构成的 ,这让操作数能被传入到ALU并在运算后输出。
数据通路时序: 就是数据执行一次ALU并输出的过程。
首先从时钟下降沿开始执行数据通路,之后在时钟上升沿将C总线的值加载到寄存器,N和Z寄存器被加载,同时如果前一个数据通路周期最后有进行的内存操作的值 ,那也会被写入到 MBR 和 MDR 中
△w:MPC 所指的地址读出微指令 加载到 MIR
△x :MIR 开始执行 , 将选中的寄存器的值 加载 到 B 总线上。
△y :ALU开始执行 并产生稳定的结果
△z : C总线 、内存总线和ALU的值变得稳定
(只要MBR开始有效,MPC的就被加载以准备下一条微指令)可不在时钟边沿就 更新
一些名词的意思
大端法 :每一个字中 字节地址 从左往右依次增大 0 1 2 3
小端法 :每一个字中 字节地址 从左往右依次减小 3 2 1 0
共同点:每个字节32位整数表示是一样的 , 但是如果是字符串 就容易出错。
符号扩展: 高位全部补上符号位
比如 1101 0000 - >1111 1111 1111 1111 1111 1111 1101 0000 8位到32位的符号扩展
MAR /MDR : 读写指令系统层的 数据字(机器字长寻址)
PC /MBR: 读写指令系统层的 可执行程序(字节寻址)
MBR1 : 保存下一个字节
MBR2 : 保存下两个字节
指令周期 : cpu取指执行的时间
机器周期 : 完成指令中 一个过程的时间(取指,译码,执行)
时钟周期 : CPU晶振频率的倒数
指令的应用
汉诺塔的汇编实现:整个递归过程的调用,栈只有一个,利用ESP来不断调动位置达到每次函数调用互不影响。
提高CPU运行速度的设计
1.cache, 在cpu与内存之间 加上几级cache 利用程序的时间局部性和空间局部性,加快访存。
-
cache和主存每次传递是以一个cache块为单位
-
最常用的是组相连映射(类似于开链法hash) :设主存大小 M , Cache 大小为C 。 则 一共有 0 -→ M % C 共M%C + 1组 (这就是直接映射,容易冲突),为了降低冲突就让每个组中 可以有多组冲突的数据,2路、4路 就代表每组可有两个、4个 主存地址%C 之后一样的地址。
2.带预取的设计(指令流水) :由IFU 完成 从内存中取下一条指令,并直接完成PC++,保证MBR2,MBR1保存是下一个指令的值。
可以达到一直有指令在缓存区的效果, 而不用等一条指令执行完成在取指再去内存取指。
3.超标量: 一个时钟周期 启动多条指令,一条流水线,在执行时采用流水。
4. 流水线设计(微指令流水):就是尽可能的减少停止的周期
- 将足够的微指令先译码出来到一个队列中 ,然后一个一个的放入MIR1,每次多个MIR 向后赋值(3->4,2->3,1->2) 这样就可以实现七段流水,有七条微指令一直都在执行
- 七段流水: 取指 →译码 → 队列 → MIR1 → MIR2 →MIR3→MIR4
5. 分支预测 : 预测goto 把预测的结果等 先存到“秘密寄存器,最后根据预测正确 copy ,失败 换一个方向执行。
- 动态预测 eg : cpu维护一张历史表 ,连续两次预测失败就更换
- 静态预测 eg: 向下goto的 为true , 向上goto 的为false
- 编译器预测 eg: 枚举性循环
6. 乱序执行 + 寄存器重命名 :
- 所有指令可以在任意时刻 发出,如果要保证精确中断必须得按顺序执行完成, 要规避RAW , WAR , WAW 的影响。
- 用记分牌 保存 在被读入的寄存器、在写入的寄存器, 可判断相关性。
- 可以用一些秘密的寄存器 ,去替代那些 有上述三个相关的 寄存器 进行读写
7. 推测执行 :先执行一些 可能会执行到的 较慢的语句
- 加载语句 : cache 中找不到 就放弃 , 访存太慢了,代价大,但如果之后确实要访存那也只能访存了。
- 可能有陷阱语句 : 给寄存器加一个毒性位 ,猜测时,如果产生陷阱,就给毒性外置1 如果这个毒性位之后没有被用到就不产生陷阱,并且会被清空。
8.在IA-64采用了判定,指令调度,推测加载
将不少运行时判断正确性的检测,放到了编译时。这样的并行性更高,但是编译器优化有很大难度。
- 判定 :减少条件判断 ,相等P4就为1,则P5就为0.这样子流水的时候,如果预测失败,就不用倒回去,只要把这条指令产生的结果不写回即可
- 指令调度:让编译器分成n组,每组中的指令相互之间互冲突,CPU就可以任意调度
- 推测加载:编译器和寄存器的失效位 通过作用与推测执行。