一条CPU指令的执行,需要经历取得指令、指令译码、执行指令,如果一个指令在一个时钟周期内完成,那么指令的执行时间有长有短,时钟周期与主频有关,如果要保证每条指令都在一个时钟周期内完成的话,主频就要设置的和执行时间最长的指令一致,这就是单指令周期处理器。
在这种情况下 我们的主频就不能太高,太高的话 例如复杂指令的执行结果就不会写入内存,那么结果就是下一条指令执行的结果不准
指令流水线
把指令分成不同的阶段,每一个阶段的电路在完成对应的任务之后,不需要等待整个指令执行完成,可以直接执行下一条指令。
指令执行拆分成ALU计算,内存访问-数据写回,就可以变成5级流水线
五级流水线就表示我们在同一个时钟周期里,同时运行五条指令的不同阶段。这是执行一条指令的时钟周期变成了5,但是CPU的主频提的更高了。这样不需要确保最复杂的那条指令在一个时钟周期里完成,而只要保障一个最复杂的流水线级的操作,在一个时钟周期内完成就好了。
把指令拆分成不同的阶段后,每一个阶段执行的结果就需要存起来,存储在流水线寄存器中,级数越多,花费的时间就会越多,所以现代CPU 14级 20级
流水线技术并不能缩短单指令的响应时间,但是可以增加在运行很多条指令的吞吐率。同一时间内执行的指令数变多了,所以提升了吞吐率。
主频上去了 相应的功耗也就大了
数据冒险
int a = 10 + 5; // 指令1
int b = a * 2; // 指令2
float c = b * 1.0f; // 指令3
上述代码指令1 指令2 指令3之间存在以来关系,如果1 执行时间是200ps 2执行的是300ps 3 执行的是600ps ,理想状态下流水线执行应该是 800ps 但是由于这3条指令之间存在依赖关系,所以结果就是 1100ps 此时的性能并没有提升太多,和单指令没太大差别
乱序执行的技术可以解决这个问题,可以把没有关联的指令优先执行,但是如果流水线级数太长,比如20级 想找到20级都没有关联的程序还是难找的。
一条指令的执行可以分为五大部分
一、结构冒险
在执行两条指令的不同阶段中,A指令执行到取得指令要访问内存 B指令执行到访问内存 那么此时就出现了资源竞争
现代CPU 是把 高速缓存做了区分 分成了指令缓存和数据缓存
二、数据冒险
先写后读
先读后写
写后再写
程序必须保证顺序执行
解决数据冒险的办法
现代更高级的做法是操作数前推 也叫操作数旁路或者操作数转发 就是通过硬件层面制造一条旁路,让一条指令的执行结果直接输出给下一条指令,不需要让第二条指令再等待第一条指令数据写回寄存器,第二条指令再读取寄存器。但是这种办法也不能解决所有的冒泡
乱序执行
简单说就是把没有依赖的指令先执行 不必要等待前边的指令都执行完
原理:
1. 取指令和指令译码 和 以前一样
2. 指令译码完成之后 指令不会立马执行 而是会保存在一个保留站中
3. 这些指令等依赖他们的数据都给到他们之后才会执行
4. 交给ALU执行
5. 指令执行完成之后 并不会立刻把数据写回寄存器 而是会保存到一个重排序缓冲区
6. 在重排序缓冲区里,CPU会根据取指令的顺序 对结果进行排序,只有前边的指令都执行完了 才会提交数据
7. 最终的数据会先到存储缓冲区,最终才会到高速缓存和内存中
三、控制冒险
为了能确保能取到正确指令,而不得不进行延迟的情况就是控制冒险,比如说:jmp jle这样的跳转指令,只有跳转指令执行完成之后 更新了PC寄存器之后 才能知道是顺序执行下一条指令 还是跳转到另外一个地址 取别的指令。
分支预测
假装分支不发生
动态分支预测
看如下代码
public class BranchPrediction {
public static void main(String args[]) {
long start = System.currentTimeMillis();
for (int i = 0; i < 100; i++) {
for (int j = 0; j <1000; j ++) {
for (int k = 0; k < 10000; k++) {
}
}
}
long end = System.currentTimeMillis();
System.out.println("Time spent is " + (end - start));
start = System.currentTimeMillis();
for (int i = 0; i < 10000; i++) {
for (int j = 0; j <1000; j ++) {
for (int k = 0; k < 100; k++) {
}
}
}
end = System.currentTimeMillis();
System.out.println("Time spent is " + (end - start) + "ms");
}
}
上边两个循环执行的时间 分别是 5ms 和 15ms 为什么会有这么大的差别
for循环其实也是com和jle这样的跳转指令 分支预测就是假定跳转不发生 那么对于第一段程序来讲 只有 1001000 次预测错误 10000次预测都是正确的 而第二段程序却有100001000次预测错误 因此时间肯定是长的
一、多发射与超标量
通过增加译码器和取指令的硬件操作在同一个时钟周期内完成多个指令
二、安腾 超长指令的失败
超长指令字 可以提前一次取一个指令包
CPU想再同一个时间并行执行两条指令 但是两条指令在代码里是有先后顺序的,无论之前流水线架构、分支预测、乱序执行、还是超标量和超长指令,都是同一时间执行两条指令的不同阶段 来提升吞吐率
一、超线程技术
超线程CPU 是把一个物理层面的CPU核心,伪装成两个逻辑层面的CPU核心。
例如一个物理CPU核心内部,会有双份的PC寄存器、指令寄存器、和条件码寄存器。这样CPU就可以维护两条并行的指令状态,在外界看来似乎有两个逻辑层面的CPU在运行
其他层面的东西 例如 指令译码器,ALU 都不会有两份,超线程只是在一个线程A在流水线停顿的时候去执行另外一个线程的指令。当然A和B是没有相互依赖的。
这种技术只在特定的场景下有用,一般在各个线程之间等待时间长的情况下有用
二、单指令多数据流技术(SIMD)
例如Numpy 直接使用了SIMD指令
简单说就是计算是并行的 一次可以读取多个数据做计算,比一个一个取当然要快的多了
一、异常
由硬件和软件组合到一起的处理过程,异常的前半生,也就是异常的发生和捕捉,是在硬件层面完成的,但是异常发生的后半生 也就是异常的处理阶段是在软件层面发生的
计算机会为每一种可能发生的异常分配一个异常代码,拿到异常代码后,计算机就会触发异常处理流程,计算机里会保留一个异常表(或者中断向量表),存放的是不同的异常代码对应的异常处理程序所在的地址。
二、异常分类
中断
例如我们按下键盘 触发一个CPU指令 CPU里的开关发生了变化 就触发了一个中断类型的异常
陷阱
例如打个断点 例如 程序通过系统调用去读取文件,创建进程。用户态的应用程序没有权限来做这件事,需要把对应的流程转交给有权限的异常处理程序来进行。
故障
例如 加法溢出 故障和中断区别就是 故障执行完之后 还要回来继续执行当前指令,因为当前指令并没有执行完。
中止
中止是故障的一种 当遇到故障CPU恢复不回来的时候,程序就中止了
三、上下文切换
当异常发生时 CPU就会触发异常处理程序,这时就像函数调用栈一样将异常处理程序压栈 同时要把寄存器里的内容都放到栈里 更像是独立进程之间的切换 这个过程被称为上下文切换。
一、复杂指令集CISC 精简指令集RISC
在RISC架构里 CPU把指令精简到20%的简单指令,原先复杂的指令则通过简单指令组合起来来实现。
二、微指令架构
在微指令架构里,编译器编译出来的机器码和汇编代码没有发生变化,但是在指令译码的阶段,翻译出来的不再是某一条CPU指令,而是翻译成好几条微指令。也就是固定长度的RISC风格了。然后放到缓冲区中,再从缓冲区里分发到后面的超标量和乱序执行的流水线架构里。
三、由复杂指令到精简指令翻译过程造成的时间浪费
解决办法就是使用缓存,CPU中大部分时间都是在运行那20%的指令,所以增加了L0cache,保存CISC到RISC指令的翻译结果,提升了性能。
intel在CPU层面做了大量的优化,例如乱序执行 分支预测等工作,x86的cpu在功耗上始终远远高于ARM架构
四、ARM 战胜 intel