(三)cpu内部如何处理代码的执行

1、一个最简单cpu的数据通路

(三)cpu内部如何处理代码的执行_第1张图片

可以看到,cpu内部一直重复执行着 Fetch(取指令)-->  decode(指令译码)--> execute(执行指令),这个循环叫做指令周期pc寄存器中存储的地址,需要地址译码器来寻址,在偌大的内存中找到对应地址存储的指令后,存入指令寄存器,再通过指令译码器把指令翻译成各个线路的控制信号给到运算器(运算器ALU是没有状态的,只能根据输入计算并输出结果),运算器输出结果,再结果写入到存储器中。

2、cpu是如何做到自动重复执行,如何实现存储,如何让pc寄存器自动增加

我们先来看一下左边这个简单的电路,开关B原本是闭合的,我们把开关A合上后,开关B则会不停的断开和闭合。对于下游电路来说就是不断产生0和1这样的信号,这就是我们的时钟信号。我们只需要在每次电路接通时对pc寄存器加1,即可以实现每间隔一个时钟周期 pc寄存器就会加1。

(三)cpu内部如何处理代码的执行_第2张图片

有了震荡电路,我们还需要解决数据的存储问题。请看图1,开关全部断开输出为0,合上开关R输出为1,断开开关R输出仍为1,再合上开关S输出则变为0可以看到这个电路开关都断开时的输出结果与上一次的操作有关。这就是记忆功能,该电路能存储一个bit的信息。(如图2,对电路再做一些完善,加上时钟信号,并只提供一个输入端,实现对一个bit的读写,我们叫做D型控制器)

有了时钟信号,D型控制器,再加上加法器即可以实现pc寄存器的自增了。

(三)cpu内部如何处理代码的执行_第3张图片

3、cpu分级设计提升性能

有了时钟信号,cpu就能实现自动重复执行: Fetch(取指令)-->  decode(指令译码)--> execute(执行指令)。每一个时钟周期,程序计数器就会加1,对于单指令周期处理的方式, 显然在这个时钟周期内我们必须完成处理一个指令的所有步骤。因为下一个时钟周期来临,我们必须要处理下一条指令了。这种处理指令的方式很简单,但是有一个最大的弊端,一个时钟周期必须保证处理完一条最复杂的指令,那么当处理简单的指令时就会浪费很多的时间

我们知道在处理指令时,不同的操作阶段使用到cpu中不同的组件,我们把这一整套处理操作比作成工厂里的流水线。我们只需要保证一个时钟周期内执行完一个最复杂的操作即可,这就是流水线分级处理,如下图所示。这样我们就充分的利用了每个组件,提升处理指令的效率。

(三)cpu内部如何处理代码的执行_第4张图片

4、cpu分级后的挑战

虽然分级后明显提升了cpu的效率,但是缺也带来了很多的挑战。

  • 功耗:分级了后则线路变得复杂,数据的存储变得更多,功耗资源消耗更大
  • 结构冒险:可能存在多个步骤之间同时执行时争抢资源
  • 数据冒险:多个指令之间的数据存在依赖关系,先读后写,先写后读,先写再写
  • 出现if else时:指令并非是顺序执行的,那么分级后默认的顺序执行就可能出错

针对这些问题,我们提出了一些解决方案

  • 针对功耗的问题,我们只需要控制好分级的层数即可,目前流水线级数已经达到了14级
  • 针对资源冲突,我们可以增加资源
  • 针对数据冒险,最简单的办法就是在指令中插入等待操作NOP。但是单纯的停顿等待太过于浪费时间,我们可以使用操作数前推的方式,把上一个计算的结果,直接转发到下一个指令的执行阶段。这样我们需要多拉出一根信号传输线路。但是这样可以省去把结果写入寄存器,再读取出来的步骤的时间。提前执行下一条指令。但是也没法完全避免等待,毕竟还是需要等待上一个指令把结果计算出来。

(三)cpu内部如何处理代码的执行_第5张图片

  • 乱序执行在等待阶段,某个部件其实是空闲的,那么后面的指令若没有依赖关系,则可以不用等前面的指令,自己先执行。乱序执行实现比较复杂,大致情况如下:

1、取指令和指令译码还是顺序执行的,但是译码完成后CPU 不会直接进行指令执行,而是进行一次指令分发,把指令发到一个叫作保留站的地方。

2、保留站中的这些指令不会立刻执行,而要等待它们所依赖的数据,传递给它们之后才会执行。一旦指令依赖的数据来齐了,指令就可以交到后面的功能单元,其实就是 ALU,去执行了。我们有很多功能单元可以并行运行,但是不同的功能单元能够支持执行的指令并不相同。

3、指令执行的阶段完成之后,我们并不能立刻把结果写回到寄存器里面去,而是把结果再存放到一个叫作重排序缓冲区的地方。在重排序缓冲区里,我们的 CPU 会按照取指令的顺序,对指令的计算结果重新排序。只有排在前面的指令都已经完成了,才会提交指令,完成整个指令的运算结果。

4、 实际的指令的计算结果数据,并不是直接写到内存或者高速缓存里,而是先写入存储缓冲区(Store Buffer 面,最终才会写入到高速缓存和内存里。

(三)cpu内部如何处理代码的执行_第6张图片

  • 如果存在 if else 这种代码,那么取指令和译码就不能没有停顿一直执行下去了。因为我们无法得知下一条指令存储的地址。if else 的逻辑对应到指令 cmp(比较), jmp(跳转)。只有等到 jmp 执行完后去更新了pc寄存器,我们才能去取下一条指令。此处则用到了分支预测方案进行处理:

1、静态预测,假装分支不发生,直接往下执行,成功的概率百分之五十,命中率太低。

2、动态预测,根据前面的结果来判断后面的分支跳转,成功的概率大大提高。 例如for 循环,第一次不跳转,则预测下一次也不跳转。

3、预测错误处理当上一条指令真正的分支判断结果出来后,发现预测错误,则需要清除掉已经执行了一半的操作,重新取指令并译码执行。只要去除指令代价不大就是很划算的。

4、分支预测的例子,代码如下,同样循环了十亿次,第二段程序比第一段程序多花费的好几倍时间。这个差异就来自我们上面说的分支预测。看下图可以发现第一段程序预测错误10万次,而第二段程序预测错误了1000万次。

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");
    }
}

(三)cpu内部如何处理代码的执行_第7张图片

你可能感兴趣的:(计算机组成原理)