目录

计算机组成学习笔记(五)

摘要
计算机组成学习笔记(五)。

6 流水线处理器

6.1 流水线的基本原理

/images/Computer_Organization/6%E6%B5%81%E6%B0%B4%E7%BA%BF%E5%A4%84%E7%90%86%E5%99%A8/6.1%E6%B5%81%E6%B0%B4%E7%BA%BF%E7%9A%84%E5%9F%BA%E6%9C%AC%E5%8E%9F%E7%90%86/%E9%9D%9E%E6%B5%81%E6%B0%B4%E7%BA%BF%E6%93%8D%E4%BD%9C.png
非流水线操作
/images/Computer_Organization/6%E6%B5%81%E6%B0%B4%E7%BA%BF%E5%A4%84%E7%90%86%E5%99%A8/6.1%E6%B5%81%E6%B0%B4%E7%BA%BF%E7%9A%84%E5%9F%BA%E6%9C%AC%E5%8E%9F%E7%90%86/%E6%B5%81%E6%B0%B4%E7%BA%BF%E7%9A%84%E6%93%8D%E4%BD%9C.png
  1. 大厨很全能,什么都会干。

  2. 新雇的这四位每一位只会干一种工作,

  3. 他们每一个人在做完自己手头的工作之后,将完成的成果交给下一个阶段, 继续进行下一步的操作。

  4. 这样的交接需要一个统一的指挥,还得有一个发号施令的人。

  5. 这司号员要改成每一分钟吹一次号,

    流水线上的工作人员每听到号响,就将自己的工作成果 转交给下一阶段的人,当然,他必须保证在号响之前, 已经顺利地完成了自己的工作。 这个司号员就好比CPU当中的时钟。


/images/Computer_Organization/6%E6%B5%81%E6%B0%B4%E7%BA%BF%E5%A4%84%E7%90%86%E5%99%A8/6.1%E6%B5%81%E6%B0%B4%E7%BA%BF%E7%9A%84%E5%9F%BA%E6%9C%AC%E5%8E%9F%E7%90%86/%E6%B5%81%E6%B0%B4%E7%BA%BF%E7%9A%84%E6%93%8D%E4%BD%9C0.png
  • 第一道菜的原料送到洗菜的环节, 这时候后面的各个环节都处于空闲状态。
/images/Computer_Organization/6%E6%B5%81%E6%B0%B4%E7%BA%BF%E5%A4%84%E7%90%86%E5%99%A8/6.1%E6%B5%81%E6%B0%B4%E7%BA%BF%E7%9A%84%E5%9F%BA%E6%9C%AC%E5%8E%9F%E7%90%86/%E6%B5%81%E6%B0%B4%E7%BA%BF%E7%9A%84%E6%93%8D%E4%BD%9C1.png
  • 一分钟之后,洗菜完成,第一道菜的原料进入切菜环节, 与此同时,第二道菜的原料进入洗菜环节。
/images/Computer_Organization/6%E6%B5%81%E6%B0%B4%E7%BA%BF%E5%A4%84%E7%90%86%E5%99%A8/6.1%E6%B5%81%E6%B0%B4%E7%BA%BF%E7%9A%84%E5%9F%BA%E6%9C%AC%E5%8E%9F%E7%90%86/%E6%B5%81%E6%B0%B4%E7%BA%BF%E7%9A%84%E6%93%8D%E4%BD%9C2.png
  • 再过了一分钟,第一道菜已经切完了,与此同时,第二道菜也洗完了,当迎来下一次号响的时候,第一道菜就会进入炒菜环节, 第二道菜进入切菜环节,同时第三道菜进入洗菜环节。
/images/Computer_Organization/6%E6%B5%81%E6%B0%B4%E7%BA%BF%E5%A4%84%E7%90%86%E5%99%A8/6.1%E6%B5%81%E6%B0%B4%E7%BA%BF%E7%9A%84%E5%9F%BA%E6%9C%AC%E5%8E%9F%E7%90%86/%E6%B5%81%E6%B0%B4%E7%BA%BF%E7%9A%84%E6%93%8D%E4%BD%9C3.png
  • 然后再过一分钟,四道菜都进入了流水线当中, 这个时候整条流水线的各个环节都开始工作了,那我们就可以说,这个流水线已经被填满了,而之前的这个过程就是填充流水线的过程。
/images/Computer_Organization/6%E6%B5%81%E6%B0%B4%E7%BA%BF%E5%A4%84%E7%90%86%E5%99%A8/6.1%E6%B5%81%E6%B0%B4%E7%BA%BF%E7%9A%84%E5%9F%BA%E6%9C%AC%E5%8E%9F%E7%90%86/%E6%B5%81%E6%B0%B4%E7%BA%BF%E7%9A%84%E6%93%8D%E4%BD%9C4.png
  • 再过一分钟,第一道菜就完成了所有的工序,可以上菜了。而且之后每过一分钟,我们都可以上一道菜。
/images/Computer_Organization/6%E6%B5%81%E6%B0%B4%E7%BA%BF%E5%A4%84%E7%90%86%E5%99%A8/6.1%E6%B5%81%E6%B0%B4%E7%BA%BF%E7%9A%84%E5%9F%BA%E6%9C%AC%E5%8E%9F%E7%90%86/%E6%B5%81%E6%B0%B4%E7%BA%BF%E7%9A%84%E6%93%8D%E4%BD%9C5.png /images/Computer_Organization/6%E6%B5%81%E6%B0%B4%E7%BA%BF%E5%A4%84%E7%90%86%E5%99%A8/6.1%E6%B5%81%E6%B0%B4%E7%BA%BF%E7%9A%84%E5%9F%BA%E6%9C%AC%E5%8E%9F%E7%90%86/%E6%B5%81%E6%B0%B4%E7%BA%BF%E7%9A%84%E6%93%8D%E4%BD%9C6.png /images/Computer_Organization/6%E6%B5%81%E6%B0%B4%E7%BA%BF%E5%A4%84%E7%90%86%E5%99%A8/6.1%E6%B5%81%E6%B0%B4%E7%BA%BF%E7%9A%84%E5%9F%BA%E6%9C%AC%E5%8E%9F%E7%90%86/%E6%B5%81%E6%B0%B4%E7%BA%BF%E7%9A%84%E6%93%8D%E4%BD%9C7.png
  • 7分钟后,这四道菜都完成了。那因为我们目前的任务只有四道菜,所以在刚才流水线 被填满之后,又经历了一个流水线被排空的过程。当然如果这餐馆的客人很多,源源不断的有要做的菜单送来,那这个流水线就可以一直保持着充满的状态,每分钟都能送出一盘菜来。

/images/Computer_Organization/6%E6%B5%81%E6%B0%B4%E7%BA%BF%E5%A4%84%E7%90%86%E5%99%A8/6.1%E6%B5%81%E6%B0%B4%E7%BA%BF%E7%9A%84%E5%9F%BA%E6%9C%AC%E5%8E%9F%E7%90%86/%E6%B5%81%E6%B0%B4%E7%BA%BF%E7%9A%84%E6%80%A7%E8%83%BD%E5%88%86%E6%9E%90.png
  1. 现在我们采用这样的流水线的方式,做四道菜用了7分钟,平均每道菜用时不到2分钟,而且在流水线填满之后,可以做到每一分钟上一道菜。而之前采用非流水线的方式,是每四分钟才能上一道菜, 那如果我们能保证流水线长期处于填满的状况,那现在的性能就可以达到原先的4倍,而我们的硬件资源的投入并没有明显的变化。
  2. 采用流水线的方式,虽然可以做到每分钟上一道菜,但是单独针对某一道菜,其实还是需要4分钟,这个时间并没有缩短。

/images/Computer_Organization/6%E6%B5%81%E6%B0%B4%E7%BA%BF%E5%A4%84%E7%90%86%E5%99%A8/6.1%E6%B5%81%E6%B0%B4%E7%BA%BF%E7%9A%84%E5%9F%BA%E6%9C%AC%E5%8E%9F%E7%90%86/%E5%8D%95%E5%91%A8%E6%9C%9F%E5%A4%84%E7%90%86%E5%99%A8.png /images/Computer_Organization/6%E6%B5%81%E6%B0%B4%E7%BA%BF%E5%A4%84%E7%90%86%E5%99%A8/6.1%E6%B5%81%E6%B0%B4%E7%BA%BF%E7%9A%84%E5%9F%BA%E6%9C%AC%E5%8E%9F%E7%90%86/MIPS%E6%8C%87%E4%BB%A4%E6%89%A7%E8%A1%8C%E7%9A%84%E4%B8%BB%E8%A6%81%E6%AD%A5%E9%AA%A4.png /images/Computer_Organization/6%E6%B5%81%E6%B0%B4%E7%BA%BF%E5%A4%84%E7%90%86%E5%99%A8/6.1%E6%B5%81%E6%B0%B4%E7%BA%BF%E7%9A%84%E5%9F%BA%E6%9C%AC%E5%8E%9F%E7%90%86/%E6%89%A7%E8%A1%8C%E6%8C%87%E4%BB%A4%E7%9A%84%E4%B8%BB%E8%A6%81%E6%AD%A5%E9%AA%A4.png
  1. 取指的阶段,就是用PC的值去访问指令存储器,从而得到指令的编码,同时还需要生成PC的更新值。
  2. 在译码阶段,不仅需要把指令编码进行分解,而且还需要从寄存器堆当中读出所需的寄存器的值。
  3. 第三步执行,主要在ALU当中完成,对于算术逻辑运算指令,就是完成对应的运算,而对于访存指令,则是计算出访存的地址。
  4. 第四步是访存,对于load指令是从数据存储器当中读出对应的数据,而对于store指令, 则是将数据送到数据存储器当中去,而其他指令在这一步没有实质的操作。
  5. 最后一步是写回,对于要改写寄存器的指令,在这一步会将数据写入到寄存器堆当中指定的位置。
  6. 要注意的是,虽然分成了这五步,但只是为了便于描述而已,所有的信号都必须要在这条指令执行的过程中保持稳定。所以对于单周期处理器来说, 这一条指令执行的过程中,所有的信号都是必须要保持稳定的。

/images/Computer_Organization/6%E6%B5%81%E6%B0%B4%E7%BA%BF%E5%A4%84%E7%90%86%E5%99%A8/6.1%E6%B5%81%E6%B0%B4%E7%BA%BF%E7%9A%84%E5%9F%BA%E6%9C%AC%E5%8E%9F%E7%90%86/%E6%B7%BB%E5%8A%A0%E6%B5%81%E6%B0%B4%E7%BA%BF%E5%AF%84%E5%AD%98%E5%99%A8.png
  • 在每一个阶段之间添加上寄存器,这就被称为流水线寄存器。这些寄存器用于保存前一个阶段要向后一个阶段传送的所有的信息。

/images/Computer_Organization/6%E6%B5%81%E6%B0%B4%E7%BA%BF%E5%A4%84%E7%90%86%E5%99%A8/6.1%E6%B5%81%E6%B0%B4%E7%BA%BF%E7%9A%84%E5%9F%BA%E6%9C%AC%E5%8E%9F%E7%90%86/%E6%B5%81%E6%B0%B4%E7%BA%BF%E5%A4%84%E7%90%86%E5%99%A8%E7%9A%84%E6%80%A7%E8%83%BD%E5%88%86%E6%9E%901.png
  1. 在单周期的处理器上,执行一条指令需要这样五步,也就是取指、译码、执行、访存和写回。假设每个阶段都正好需要200个皮秒, 那执行完这条指令,就总共过去了1000ps,那这个单周期处理器,它的时钟周期就需要被设置为1000ps,从外界看来,这个处理器每1000ps可以完成一条指令。
  2. 对于流水线处理器,
  3. 同样也需要这五步,也同样需要花1000ps的时间。但是不同在于,在过去200ps之后,当第一条指令完成了取指阶段,而进入到译码阶段的时候,实际上取指部件已经空闲下来,我们就可以开始第二条指令的取址工作,也就是说第二条指令在此时,已经开始执行了。
  4. 这样对一个流水线处理器,虽然一条指令总共也是需要花 1000ps,但是每200ps就可以开始执行一条指令,而且当流水线填满之后,每200ps也就可以完成一条指令,所以对于这样一个流水线处理器,它的时钟周期可以设为200ps,因此,这个处理器的主频就是刚才这个单周期处理器的5倍。

/images/Computer_Organization/6%E6%B5%81%E6%B0%B4%E7%BA%BF%E5%A4%84%E7%90%86%E5%99%A8/6.1%E6%B5%81%E6%B0%B4%E7%BA%BF%E7%9A%84%E5%9F%BA%E6%9C%AC%E5%8E%9F%E7%90%86/%E6%B5%81%E6%B0%B4%E7%BA%BF%E5%A4%84%E7%90%86%E5%99%A8%E7%9A%84%E6%80%A7%E8%83%BD%E5%88%86%E6%9E%902.png
  1. 新插入的流水线寄存器,它自身也会带来一些新的延迟。
  2. 如果我们加上流水线寄存器的延迟,同样还是执行这几条指令,那就需要每隔250ps 才可以开始一条新的指令,所以时钟周期应该设为250ps,而且对于每条指令 本身来说,需要花1250ps才能够完成。在这一点上,是比刚才在单周期处理器还要更慢一些的。

/images/Computer_Organization/6%E6%B5%81%E6%B0%B4%E7%BA%BF%E5%A4%84%E7%90%86%E5%99%A8/6.1%E6%B5%81%E6%B0%B4%E7%BA%BF%E7%9A%84%E5%9F%BA%E6%9C%AC%E5%8E%9F%E7%90%86/%E6%B5%81%E6%B0%B4%E7%BA%BF%E5%A4%84%E7%90%86%E5%99%A8%E7%9A%84%E6%80%A7%E8%83%BD%E5%88%86%E6%9E%903.png
  1. 对于流水线处理器来说,因为各个处理部件可以并行 工作,从而可以使得整个程序的执行时间缩短,但是流水线并不会缩短单条指令的执行时间,相反,还会增加这个时间。
  2. 因此,采用流水线的方式,实际上是提高了指令的吞吐率,从而从整体上缩短了程序的执行时间,提高了系统的性能。

6.2 流水线的优化

/images/Computer_Organization/6%E6%B5%81%E6%B0%B4%E7%BA%BF%E5%A4%84%E7%90%86%E5%99%A8/6.2%E6%B5%81%E6%B0%B4%E7%BA%BF%E7%9A%84%E4%BC%98%E5%8C%96/%E6%B5%81%E6%B0%B4%E7%BA%BF%E7%9A%84%E5%88%86%E6%9E%90.png
流水线的分析
/images/Computer_Organization/6%E6%B5%81%E6%B0%B4%E7%BA%BF%E5%A4%84%E7%90%86%E5%99%A8/6.2%E6%B5%81%E6%B0%B4%E7%BA%BF%E7%9A%84%E4%BC%98%E5%8C%96/%E6%B5%81%E6%B0%B4%E7%BA%BF%E7%9A%84%E5%B9%B3%E8%A1%A1%E6%80%A7.png
  1. 其实我们很难做到每一个阶段恰好花同样的时间,那我们不妨假设切菜这个环节非常复杂,需要花2分钟的时间。
  2. 现在应该让这个司号手应该每两分钟才吹一次号, 因为我们这个流水线的时钟周期必须按照各流水级当中时间最长的那一级来确定。
  3. 每个阶段花费时间不相等的流水线就被称为不平衡的流水线。
  4. 虽然他还可以做到每两分钟出一道菜,但是性能提升幅度就变小了很多,而且从单独做一道菜看来,非流水线方式只需要5分钟,而用流水线方式,反而需要8分多钟,这样就慢了很多。
  5. 如果是对于流水线处理器来说,不平衡的流水线对于整体的指令吞吐率,和单独一条指令执行时间,都有非常不好的影响。因此,在划分流水线的时候,应当做到每一级所花费的时间尽可能相等,这就有可能造成这级流水线的名称和它实际所完成的工作并不完全相符。

/images/Computer_Organization/6%E6%B5%81%E6%B0%B4%E7%BA%BF%E5%A4%84%E7%90%86%E5%99%A8/6.2%E6%B5%81%E6%B0%B4%E7%BA%BF%E7%9A%84%E4%BC%98%E5%8C%96/%E6%B5%81%E6%B0%B4%E7%BA%BF%E7%9A%84%E8%B0%83%E6%95%B4.png
  1. 把切菜这个环节分为两步,既然他需要花2分钟的时间,那我们不如把它分为2个一分钟的环节,这样和其他环节的时间就变成一样的了,我们就可以设置其中周期为1分钟。
  2. 这个调整后的流水线就变成了一个平衡的流水线,对于这个流水线,我们发现,它单独做一道菜的时间已经降回到了5分钟,和非流水线的方式基本相当,更重要的是,在连续工作的情况下,它又可以做到每一分钟上一道菜,而非流水线的方式,只能是每5分钟上一道菜, 因此,采用流水线的方式,性能可以是原来的5倍。

/images/Computer_Organization/6%E6%B5%81%E6%B0%B4%E7%BA%BF%E5%A4%84%E7%90%86%E5%99%A8/6.2%E6%B5%81%E6%B0%B4%E7%BA%BF%E7%9A%84%E4%BC%98%E5%8C%96/%E8%B6%85%E7%BA%A7%E6%B5%81%E6%B0%B4%E7%BA%BF.png
  1. 将五级流水线作为一个基本的流水线划分,如果在五级流水线的基础上,将其中一些流水级细分为更多的阶段,从而增加了流水线的深度,这样的流水线就会被称为超级流水线。
  2. 超级流水线就可以做到更高的时钟频率,从而提高了指令的吞吐率。
  3. 基础的五级流水线,其中 每个流水级的组合电路的延迟大约为200ps,而流水线寄存器的延迟为50ps, 那这个流水线处理器的时钟周期就是250ps。而如果我们做一个十级的流水线,而且恰好能将这个五级流水线当中的每一级平均地切为两段,那这个处理器的时钟周期就是100ps,加上流水线寄存器的50ps,一共是150ps。那显然,使用这样的超级流水线技术可以带来明显的性能提升。

/images/Computer_Organization/6%E6%B5%81%E6%B0%B4%E7%BA%BF%E5%A4%84%E7%90%86%E5%99%A8/6.2%E6%B5%81%E6%B0%B4%E7%BA%BF%E7%9A%84%E4%BC%98%E5%8C%96/%E6%B5%81%E6%B0%B4%E7%BA%BF%E7%9A%84%E6%B7%B1%E5%BA%A6.png
  1. 对于五级流水线来说,其执行单条指令的延迟是1250ps,而对于这个十级流水线,它执行单条指令的延迟就变成了1500ps。
  2. 切分流水线之后,提高了时钟的频率,从而也提高了指令的吞吐率,但是单条指令的执行时间确实变长了的,这是因为我们增加了更多的流水线寄存器。
  3. 在五级流水线当中,流水线寄存器的延迟大约占20%的比例, 而在十级流水线当中,因为每级的组合逻辑电路的延迟减半了,但是流水线寄存器的延迟是不会发生变化的,因此,流水线级数划分的越多,流水线寄存器的延迟所占的比例就会越高, 从而导致单条指令的延迟越来越大。
  4. 当流水线级数变多之后,填满一个流水线所需要的指令就会变多,而这些同时处在流水线当中的指令,他们之间的关系 也就会变得更加复杂,从而会带来更多的负面影响。

/images/Computer_Organization/6%E6%B5%81%E6%B0%B4%E7%BA%BF%E5%A4%84%E7%90%86%E5%99%A8/6.2%E6%B5%81%E6%B0%B4%E7%BA%BF%E7%9A%84%E4%BC%98%E5%8C%96/%E5%A4%84%E7%90%86%E5%99%A8%E6%B5%81%E6%B0%B4%E7%BA%BF%E6%B7%B1%E5%BA%A6%E7%9A%84%E5%8F%98%E5%8C%96.png

6.3 超标量流水线

/images/Computer_Organization/6%E6%B5%81%E6%B0%B4%E7%BA%BF%E5%A4%84%E7%90%86%E5%99%A8/6.3%E8%B6%85%E6%A0%87%E9%87%8F%E6%B5%81%E6%B0%B4%E7%BA%BF/%E8%B6%85%E6%A0%87%E9%87%8F%E6%B5%81%E6%B0%B4%E7%BA%BF.png
超标量流水线
/images/Computer_Organization/6%E6%B5%81%E6%B0%B4%E7%BA%BF%E5%A4%84%E7%90%86%E5%99%A8/6.3%E8%B6%85%E6%A0%87%E9%87%8F%E6%B5%81%E6%B0%B4%E7%BA%BF/%E8%B6%85%E5%8F%98%E9%87%8F%E6%B5%81%E6%B0%B4%E7%BA%BF%E7%9A%84%E6%93%8D%E4%BD%9C0.png /images/Computer_Organization/6%E6%B5%81%E6%B0%B4%E7%BA%BF%E5%A4%84%E7%90%86%E5%99%A8/6.3%E8%B6%85%E6%A0%87%E9%87%8F%E6%B5%81%E6%B0%B4%E7%BA%BF/%E8%B6%85%E5%8F%98%E9%87%8F%E6%B5%81%E6%B0%B4%E7%BA%BF%E7%9A%84%E6%93%8D%E4%BD%9C1.png /images/Computer_Organization/6%E6%B5%81%E6%B0%B4%E7%BA%BF%E5%A4%84%E7%90%86%E5%99%A8/6.3%E8%B6%85%E6%A0%87%E9%87%8F%E6%B5%81%E6%B0%B4%E7%BA%BF/%E8%B6%85%E5%8F%98%E9%87%8F%E6%B5%81%E6%B0%B4%E7%BA%BF%E7%9A%84%E6%93%8D%E4%BD%9C2.png
  1. 同时就可以进行两道菜的操作。
  2. 一分钟过去之后,洗菜这个环节就会将这两份菜的原料分别送到切菜1这个环节的两位操作人员手中,而与此同时第三道菜和第四道菜则会进入洗菜环节。
  3. 这样在每一个环节都有两道菜菜在同时的并行向前, 那到了五分钟的时候就可以同时完成两道菜。
  4. 对于处理器,这也常被称为双发射的结构。

/images/Computer_Organization/6%E6%B5%81%E6%B0%B4%E7%BA%BF%E5%A4%84%E7%90%86%E5%99%A8/6.3%E8%B6%85%E6%A0%87%E9%87%8F%E6%B5%81%E6%B0%B4%E7%BA%BF/Pentium%E7%9A%84%E8%B6%85%E6%A0%87%E9%87%8F%E6%B5%81%E6%B0%B4%E7%BA%BF.png
Pentium的超标量流水线
/images/Computer_Organization/6%E6%B5%81%E6%B0%B4%E7%BA%BF%E5%A4%84%E7%90%86%E5%99%A8/6.3%E8%B6%85%E6%A0%87%E9%87%8F%E6%B5%81%E6%B0%B4%E7%BA%BF/CDC6600.png
CDC6600
/images/Computer_Organization/6%E6%B5%81%E6%B0%B4%E7%BA%BF%E5%A4%84%E7%90%86%E5%99%A8/6.3%E8%B6%85%E6%A0%87%E9%87%8F%E6%B5%81%E6%B0%B4%E7%BA%BF/Cortex-A9%E7%9A%84%E8%B6%85%E6%A0%87%E9%87%8F%E6%B5%81%E6%B0%B4%E7%BA%BF.png
  1. 它每个时钟周期可以发射四条指令, 根据指令的不同,总共会经过八到十一级流水线。
  2. 与奔腾类似的是在流水线的前端比如说取指,译码并没有分成多条流水线而是采用统一的部件。当然我们要知道这些部件虽然看上去是一个,但它实际上比标量流水线要大得多,比如说取指部件至少一次要能取来四条指令甚至更多,而译码部件一次也至少应该完成四条指令的译码。

/images/Computer_Organization/6%E6%B5%81%E6%B0%B4%E7%BA%BF%E5%A4%84%E7%90%86%E5%99%A8/6.3%E8%B6%85%E6%A0%87%E9%87%8F%E6%B5%81%E6%B0%B4%E7%BA%BF/Core%20i7%E7%9A%84%E8%B6%85%E6%A0%87%E9%87%8F%E6%B5%81%E6%B0%B4%E7%BA%BF.png
  1. 指令高度缓存(L1 Instruction Cache),也就相当于我们在流水线原理当中提到的指令存储器,我们可以看到每个周期从指令存储器当中会取回128个比特,也就是十六个字节,因为x86指令长度是不固定的,所以首先要经过一个 指令长度的译码器,分解出到底哪几个字节是一条指令。那么在这一点上RISC指令系统就体现出了明显的优势,它每条指令都是定长的,不用额外进行这样的识别工作。
  2. 在译码器(Instruction Decoders)当中通过硬件会将x86的指令转换成更为简单的指令,这些指定被称为微操作,那从这里可以看出有三个简单的译码器(3 simple),用于对那些比较简单的x86指令进行转换,每条指令对应一个微操作。而那些非常复杂的指令则会通过这个复杂的译码器(1 complex)转换成多条微操作,而这些微操作都是类似于RISC指令的格式, 这样在它流水器的后半部分看到的都是RISC格式的简单指令了。
  3. 后半部分有多条并行的流水线,而且因为在这个流水线当中运行的是微操作,都是采用了RISC的编码风格,所以这里也可以充分运用大量面向RISC处理器研发出的高级流水线的技术。
  4. 这也就是为什么我们现在经常说x86虽然是一个CISC的指令系统, 但它实际上是用RISC的方式去实现的。

/images/Computer_Organization/6%E6%B5%81%E6%B0%B4%E7%BA%BF%E5%A4%84%E7%90%86%E5%99%A8/6.3%E8%B6%85%E6%A0%87%E9%87%8F%E6%B5%81%E6%B0%B4%E7%BA%BF/%E8%B6%85%E6%A0%87%E9%87%8F%E6%B5%81%E6%B0%B4%E7%BA%BF%E4%B8%8E%E6%A0%87%E9%87%8F%E6%B5%81%E6%B0%B4%E7%BA%BF.png
  1. 最开始从单周期处理器到流水线处理器主要考虑的是时间并行性上的优化,通过对现有硬件进行切分,只是增加了少量的流水线寄存器以及部分的控制信号的改动,那原本串行执行的指令在一定程度上并行起来。
  2. 而从标量流水线到超标量流水线则是主要考虑了空间并行性上的优化,这是让不同的指令同时在不同的流水线上运行,那么简单的看来每增加一个发射数就需要增加一条流水线的硬件资源。

/images/Computer_Organization/6%E6%B5%81%E6%B0%B4%E7%BA%BF%E5%A4%84%E7%90%86%E5%99%A8/6.3%E8%B6%85%E6%A0%87%E9%87%8F%E6%B5%81%E6%B0%B4%E7%BA%BF/%E8%B6%85%E6%A0%87%E9%87%8F%E6%B5%81%E6%B0%B4%E7%BA%BF%E4%B8%8E%E5%A4%9A%E6%A0%B8CPU.png
  1. 首先我们来看一个概念叫做处理器核。这部分实际上就包含了 我们之前介绍的那些数据通路控制信号等等。当然还需要包含指令和数据的高度缓存对应了 我们原理结构当中的指令存储器和数据存储器。那为了提高性能现在的处理器当中一般还配备了二级的高速缓存。这些部件的关系非常紧密,我们通常也就把这一部分称为一个处理器核。那么刚才看到那个四发射十六级流水线的结构图就是在只这么一个处理器核内部的结构,那么可以说这一个处理器核就是一个超标量流水线的处理器核。
  2. 在单核的时代这个部分结构就单独制造出了一个芯片,就是以前的单核CPU,那现在我们把这样同样的结构复制多份,然后再加上一些共享的存储部件就构成了一个多核的CPU,这里面有四个核儿那就是一个四核的CPU, 每一个核内部都是一个超标量流水线的结构,这就是我们现在通常说的多核CPU和超标量流水线之间的关系。

6.4 流水线的冒险

/images/Computer_Organization/6%E6%B5%81%E6%B0%B4%E7%BA%BF%E5%A4%84%E7%90%86%E5%99%A8/6.4%E6%B5%81%E6%B0%B4%E7%BA%BF%E7%9A%84%E5%86%92%E9%99%A9/%E5%86%92%E9%99%A9.png
  1. 一是结构冒险。在这里结构是指硬件电路当中的某个部件,如果这条指令所需要的硬件部件还在为之前的指令工作无法为这条指令提供服务,那就产生了结构冒险。
  2. 第二种是数据冒险,如果这条指令需要某个数据而之前的指令正在操作这个数据,那这条指令就无法执行,这种情况称为数据冒险。
  3. 第三种是控制冒险,如果现在要执行哪条指令,是由之前指令的运行结果来决定的,而现在之前指令的结果还没有产生,那就导致了控制冒险。

/images/Computer_Organization/6%E6%B5%81%E6%B0%B4%E7%BA%BF%E5%A4%84%E7%90%86%E5%99%A8/6.4%E6%B5%81%E6%B0%B4%E7%BA%BF%E7%9A%84%E5%86%92%E9%99%A9/%E7%BB%93%E6%9E%84%E5%86%92%E9%99%A91.png
  • 第一条是一个Load的指令,后面是若干其他的指令,那么期望这些指令依次进入流水线开始执行, 那么注意在是第四个时钟周期Load的指令要从存储器中读取数据,而与此同时取直部件也要从存储器当中读取第三条指令的编码,那如果我们这个系统当中指令和数据是存放在同一个存储器当中的,而对于一个存储器在同一个时刻只能接受一个读操作,那这里就会发生结构冒险。

/images/Computer_Organization/6%E6%B5%81%E6%B0%B4%E7%BA%BF%E5%A4%84%E7%90%86%E5%99%A8/6.4%E6%B5%81%E6%B0%B4%E7%BA%BF%E7%9A%84%E5%86%92%E9%99%A9/%E7%BB%93%E6%9E%84%E5%86%92%E9%99%A92.png
  1. 既然不能同时读,那就不读好了,那在这个时钟周期首先让Load的指令去读存储器你获得它所需要的数据,而取指部件这时不读存储卡而是让流水线停顿。在所谓停顿,也不是什么都不管,必须要将相关的控制信号视为不改变及其状态的值, 那这种设置我们就称为一个空泡。
  2. 这样这个结构冒险就被消除了,而第三条指令的取指被延后到下一个周期才开始。
  3. 这样会不会和第一条指令的访存产生冲突呢? 那当然如果第一条指令也是访存指令,那还是会发生结构冒险。那流水线还需要再停顿一个周期,第三条指定要等到下一个周期再进行取指。如果连续出现几条访存指令, 那后面流水线就会连续的停顿,这样效率很低,但是从另一个角度讲这是一种非常安全 又简便的方法,用这种方法其实可以解决各种冒险。

/images/Computer_Organization/6%E6%B5%81%E6%B0%B4%E7%BA%BF%E5%A4%84%E7%90%86%E5%99%A8/6.4%E6%B5%81%E6%B0%B4%E7%BA%BF%E7%9A%84%E5%86%92%E9%99%A9/%E7%BB%93%E6%9E%84%E5%86%92%E9%99%A93.png
  • 在现在的处理器当中我们通常还是将指令和数据分别放在不同的存储器当中,就是靠在存储器当中设置独立的指令高度缓存和数据高度缓存来实现的。我们还是要强调的在计算机中主存储器也就是内存是统一存放指令和数据的,这也是冯诺依曼结构的要求,只是在CPU当中 的一级高速缓存会采用指令和数据分别存放的方式,那这种结构冒险我们现在就已经解决了。

/images/Computer_Organization/6%E6%B5%81%E6%B0%B4%E7%BA%BF%E5%A4%84%E7%90%86%E5%99%A8/6.4%E6%B5%81%E6%B0%B4%E7%BA%BF%E7%9A%84%E5%86%92%E9%99%A9/%E7%BB%93%E6%9E%84%E5%86%92%E9%99%A94.png
  • 这里就出现了两条指令同时要对一个硬件部件进行操作的情况。

/images/Computer_Organization/6%E6%B5%81%E6%B0%B4%E7%BA%BF%E5%A4%84%E7%90%86%E5%99%A8/6.4%E6%B5%81%E6%B0%B4%E7%BA%BF%E7%9A%84%E5%86%92%E9%99%A9/%E7%BB%93%E6%9E%84%E5%86%92%E9%99%A95.png /images/Computer_Organization/6%E6%B5%81%E6%B0%B4%E7%BA%BF%E5%A4%84%E7%90%86%E5%99%A8/6.4%E6%B5%81%E6%B0%B4%E7%BA%BF%E7%9A%84%E5%86%92%E9%99%A9/%E7%BB%93%E6%9E%84%E5%86%92%E9%99%A96.png
  • 这和寄存器堆本身的特性有关,相对来说寄存器堆的读写速度比较快, 我们假设读或者写寄存器的延迟为100ps,而其他部件比如说ALU的延迟就就比较大,视为200ps, 那么我们就可以在前半个时钟周期用于完成寄存器堆的写,后半个时钟周期用来完成读操作,并且在寄存器堆上设置独立的读写口。这样就可以在一个时钟周期内同时完成了读和写的操作。

/images/Computer_Organization/6%E6%B5%81%E6%B0%B4%E7%BA%BF%E5%A4%84%E7%90%86%E5%99%A8/6.4%E6%B5%81%E6%B0%B4%E7%BA%BF%E7%9A%84%E5%86%92%E9%99%A9/%E6%95%B0%E6%8D%AE%E5%86%92%E9%99%A91.png
  • 减法指令需要到第五个周期也就写回这个周期才会将运算结果写到t0寄存器当中去,而加法指令在第三个周期也就它自己的译码这个阶段就需要读出t0寄存器,那从这里就可以看出,这条加法指令需要用前一条指定的运算结果, 但是在这个时刻这个运算结果还没有写回到寄存器当中去,这就产生了数据冒险。

/images/Computer_Organization/6%E6%B5%81%E6%B0%B4%E7%BA%BF%E5%A4%84%E7%90%86%E5%99%A8/6.4%E6%B5%81%E6%B0%B4%E7%BA%BF%E7%9A%84%E5%86%92%E9%99%A9/%E6%95%B0%E6%8D%AE%E5%86%92%E9%99%A92.png
  • 根据这个流水线的结构我们需要让流水线停顿两个周期,这样在加法指令读寄存器堆的时候,减法指令已经将运算的结果写回到了t0寄存器当中去,所以加法指令读到的是正确的数值。

/images/Computer_Organization/6%E6%B5%81%E6%B0%B4%E7%BA%BF%E5%A4%84%E7%90%86%E5%99%A8/6.4%E6%B5%81%E6%B0%B4%E7%BA%BF%E7%9A%84%E5%86%92%E9%99%A9/%E6%8E%A7%E5%88%B6%E5%86%92%E9%99%A91.png
  • 在第二个时钟周期,处理器就应该去取下一条指令了,但这个时候实际上并不知道是否真的会发生分支,这条分支指令一直要到执行阶段结束,才能知道分支的条件是否成立,也就在600ps这个时候,而处理器希望在200ps的时候就去取下一条指令,这里就产生了控制冒险。
  • 因为这个取指令的动作如何进行应该由上一条地指令的运行结果来决定,而上一条指令的运行结果至少要到两个时钟周期之后才能产生

/images/Computer_Organization/6%E6%B5%81%E6%B0%B4%E7%BA%BF%E5%A4%84%E7%90%86%E5%99%A8/6.4%E6%B5%81%E6%B0%B4%E7%BA%BF%E7%9A%84%E5%86%92%E9%99%A9/%E6%8E%A7%E5%88%B6%E5%86%92%E9%99%A92.png
  • 如果单纯只想解决这个冒险,而不考虑性能的损失的话,我们还可以用那个万能的方法, 就是让流水线停顿。我们需要插入两个空泡,那么在执行阶段结束之后,我们就知道要从哪个地方开始取新的指令了。这样就可以解决这个控制冒险。

6.5 数据冒险的处理

/images/Computer_Organization/6%E6%B5%81%E6%B0%B4%E7%BA%BF%E5%A4%84%E7%90%86%E5%99%A8/6.5%E6%95%B0%E6%8D%AE%E5%86%92%E9%99%A9%E7%9A%84%E5%A4%84%E7%90%86/%E6%95%B0%E6%8D%AE%E5%86%92%E9%99%A91.png
  • 产生这个数据冒险,是因为第二条加法指令会用到第一条减法指令的运算结果。但是在流水线当中,这条加法指令在读取t0寄存器的时候, 它前一条减法指令还没有把运算结果写到t0寄存器当中去,所以这里就存在一个数据冒险。
/images/Computer_Organization/6%E6%B5%81%E6%B0%B4%E7%BA%BF%E5%A4%84%E7%90%86%E5%99%A8/6.5%E6%95%B0%E6%8D%AE%E5%86%92%E9%99%A9%E7%9A%84%E5%A4%84%E7%90%86/%E6%95%B0%E6%8D%AE%E5%86%92%E9%99%A92.png
  1. 要解决这个数据冒险,最简单的方法,实际上是在软件层面进行解决。假设我们这个处理器的流水线并不能解决这样的数据冒险, 那其实,我们只要通过编程的手段,人为的将这条加法指令退后执行,让他读取寄存器堆的时间,退后到减法指令寄存器堆之后。
  2. 我们有一条指令叫做nop,它的作用是什么也不干,我们就在这个减法指令和加法指令之间插入两个nop指令。这两个nop指令只是简单的通过流水线,并占用了相对的时间。那这样刚才的这个数据冒险至少是不存在了。而因为这两个nop指令的作用,加法指令退后了两个周期才进入流水线,那么当这条加法指令需要读寄存器堆堆时候,前面堆减法指令已经完成了对寄存器堆堆写。那加法指令就可以从寄存器堆当中读到正确堆t0的值,从而完成正确的加法运算。

/images/Computer_Organization/6%E6%B5%81%E6%B0%B4%E7%BA%BF%E5%A4%84%E7%90%86%E5%99%A8/6.5%E6%95%B0%E6%8D%AE%E5%86%92%E9%99%A9%E7%9A%84%E5%A4%84%E7%90%86/%E6%95%B0%E6%8D%AE%E5%86%92%E9%99%A93.png
  1. 到底应该几个nop指令,这是和流水线的结构相关的。如果我们这一段程序放在这个5g流水线上是正常运行的,那过几天,又出了一个更新的处理器,它的流水线是8g的,那这个程序放上去,可能运行就会发生错误。因为流水线变身之后,解决数据冒险需要的周期数可能会变多。
  2. 我们还希望对软件屏蔽硬件的这些时限细节。
  3. 我们只要发现存在这样的数据冒险,我们就在硬件的流水线上让各个控制信号都变成执行nop指令一样的值。那在这两个周期,就会产生流水线停顿的效果。而这些和nop指令效果一样的控制信号,它们所产生的状态,就成为一个空泡。那这个空泡随着时钟周期一级一级往后面传,从效果上来看,和nop指令在流水线当中一级一级的执行是一样的。只是区别在于,这样的信号是由硬件来产生的。
  4. 那现在又有了一个新的问题,如果刚才是在软件中插入了nop指令,那对于这个流水线来说,它是严格的按照取回一条指令进行执行,这样的方式来运转的。那现在需要在硬件上自动的插入空泡,那就需要一个方式来检测是否出现了数据冒险。
  5. 如果我们不是看这一段程序代码,而是看处理器当中的这五个部件,那我们怎么来判断存在数据冒险呢。所谓数据冒险,就是当前有一条指令要读寄存器, 而它之前的指令要写寄存器,但又没有完成,所以我们只用检查,在译码这个阶段,需要读的寄存器的编号, 这个通过链接在寄存器读口的信号就可以得到。然后我们再检查后面各个阶段,其实在每一级,都有些信号能够表明这条指令是否要写某个寄存器,以及要写哪个寄存器。因此,我们只需要检查后面每一个阶段所要写的寄存器的编号,和当前译码阶段,所要读读寄存器的编号,是否有相同。如果存在相同,那就是有数据冒险。那只要出现来数据冒险,我们就在流水线中插入空泡。这样我们就能通过硬件来解决数据冒险的问题。

/images/Computer_Organization/6%E6%B5%81%E6%B0%B4%E7%BA%BF%E5%A4%84%E7%90%86%E5%99%A8/6.5%E6%95%B0%E6%8D%AE%E5%86%92%E9%99%A9%E7%9A%84%E5%A4%84%E7%90%86/%E6%95%B0%E6%8D%AE%E5%86%92%E9%99%A94.png
  1. 但是,在实际的编程当中,这种先写了一个寄存器,然后很快使用的状况是经常出现的。如果说每次出现,我们都要让流水线停顿的话,对性能的影响就太大了。
  2. 减法指令在800ps之后才开始写寄存器,而加法指令最晚在500ps的时候就要去读寄存器。我们无法逆转这个时间,所以我们肯定不能把800ps才有的数送到500ps的这个时间去。但是我们可以换一个角度想一想。这条减法指令的运行结果真的是在这个时候才有的吗?实际上减法运算是在执行阶段由ALU这个部件完成的,所以最晚在600ps的这个时候,要写到t0寄存器当中到这个数已经运算完成了。所以从时间角度来看,在600ps之后,我们都可以得到t0寄存器的最新的值,而对于这条加法指令,它真的需要使用t0寄存器的值是在它的执行阶段,也就是ALU的部件需要用t0的值作为其中的一个输入, 那这个阶段是在600ps之后才开始的,我们完全可以将减法运算的结果交给这个加法运算作为输入。那这种方法,就叫做数据前递。
  3. 也就是 上一条指令将自己的运算结果往前传递到下一条指令去,那我们刚才已经分析过,在600ps的时候,ALU的输出结果已经是t0的值了,那在600ps的这个时钟上前过去之后,t0的这个值会被保存到执行和访存之间的这个流- 水线寄存器当中去。我们如果把它传递给ALU的输入,就可以正确的完成后面这条加法运算了。

/images/Computer_Organization/6流水线处理器/6.4流水线的冒险/数据冒险5.png
  1. 我们从硬件连线上可以把这个信号引回来,从新引导ALU的输入端。当然,这里我们还需要增加一个多选器, 而且我们刚才也讲过,如何去判断在流水线当中出现了数据冒险。那我们就可以用这样的判断结果作为这个多选器的选择信号, 在出现数据冒险的时候,我们选择这个前递的信号,那当然,这条加法指令也有可能在第二个原操作数上使用了t0寄存器。所以这个前递的信号还应该传送到ALU的另一个输入端, 当然在这里也需要加上多选器来进行选择,那这样的方式就被成为前递。它还有个名称叫作旁路。
  2. 从根本上来说,前递和旁路指的都是这件事情。只不过是观察和描述的角度不同而已。前递是从指令执行顺序的角度来描述的,而旁路则是从电路的结构角度来描述。本来前一条指令应该将运行的结果写入到寄存器堆,然后再交给后一条指令使用,而我们现在搭建来一条新堆通路,相当于绕过了寄存器堆,直接进行了数据堆传递,所以从硬件时限的角度来看,这是一个旁路。

/images/Computer_Organization/6%E6%B5%81%E6%B0%B4%E7%BA%BF%E5%A4%84%E7%90%86%E5%99%A8/6.5%E6%95%B0%E6%8D%AE%E5%86%92%E9%99%A9%E7%9A%84%E5%A4%84%E7%90%86/%E6%95%B0%E6%8D%AE%E5%86%92%E9%99%A96.png /images/Computer_Organization/6%E6%B5%81%E6%B0%B4%E7%BA%BF%E5%A4%84%E7%90%86%E5%99%A8/6.5%E6%95%B0%E6%8D%AE%E5%86%92%E9%99%A9%E7%9A%84%E5%A4%84%E7%90%86/%E6%95%B0%E6%8D%AE%E5%86%92%E9%99%A97.png
  1. 其实不仅仅在这个点可以建立旁路,我们在下一个流水级也可以建立旁路。
  2. 这个例子前两条指令和刚才的那个例子是一样的, 在此基础上我们又写出了第三条指令,这是一个与操作,那么它其中的一个原操作数也是t0,那我们结合实践来看,对于这条与操作指令,它真的要开始运算的时候,是在800ps之后。那在这个时候,前面- 这条减法指令已经完成了访存阶段,所以t0寄存器的最新值 现在是放在访存阶段和写回阶段之间的流水线寄存器当中的,那我们就需要用到刚才的结构图当中紫色的旁路的线,用来将t0的内容传递到ALU到输入端,从而让这条与运算指令及时端运行。
  3. 如果再往后一条指令又用到了t0,都会怎么样呢?那么这个标着3的指令在800ps之后的这个时钟周期正好进入了译码阶段,它会在这个周期的后半部分读取寄存器,那么在这个时候, 减法指令已经将t0的值写入到了寄存器堆中,所以对于这个3号指令,如果它用到了t0这个寄存器,它就可以按照正常的操作,从寄存器堆当中读出t0寄存器读值,而不需要使用前递的技术。

/images/Computer_Organization/6%E6%B5%81%E6%B0%B4%E7%BA%BF%E5%A4%84%E7%90%86%E5%99%A8/6.5%E6%95%B0%E6%8D%AE%E5%86%92%E9%99%A9%E7%9A%84%E5%A4%84%E7%90%86/%E6%95%B0%E6%8D%AE%E5%86%92%E9%99%A98.png
  1. 在这个例子当中, 前三条指令还是和刚才一样,第四条是一个load的指令,它也会用到t0寄存器,但是我们刚才已经分析过了,这个时候并不存在数据冒险。而这条load的指令是要把存储器当中的一个数取出来,存放到t1寄存器当中去。而它之后,一条或运算指令会使用t0寄存器的值,那这种情况就是一条load的指令之后跟了一条指令,会使用load的指令的目的寄存器。那在这种情况下,也会发生数据冒险。它有个专门的名称,叫作load-use冒险。
  2. 实际上是做不到用前递的技术来解决这个问题。对于这一条load的指令,我们来看要保存到t1寄存器的值,究竟是什么时候才得到的,对于刚才的运算指令,需要写回寄存器的值,是在执行阶段,也就是同过ALU运算而得。但是对于load的指令,用ALU是计算要访存的地址,而要写回寄存器堆堆数,是在访存阶段的结束才会得到, 所以是在1400ps这个地方,我们才会得到t1寄存器的值。而对于下面这一条或运算指令,我们最晚也得在1200ps这个地方,得到t1这个寄存器的值,从而让ALU可以进行正确的运算。因此,这就要求我们将1400ps这个地方得到的数,传递到之前1200ps这个时刻。
  3. 既然我们不能返回到更早的时间,那我们只能让这条或运算指令多等一个周期,这样它就可以在1400ps之后才需要这个t1寄存器的值。而此时,load指令已经完成了从数据存储器当中取出数的操作,这就可以通过刚才我们已经建立的第二组旁路通道,也就是用紫色的连线表达的这个旁路通道,将t1寄存器的内容传送到ALU的输入端口。那当然,既然我们要让或运算指令延后一个周期,我们就必须在流水线中插入空泡,让流水线产生一次停顿,所以对于这种冒险,我们需要用流水线停顿再加上数据前递的方式来解决。

6.6 控制冒险的处理

/images/Computer_Organization/6%E6%B5%81%E6%B0%B4%E7%BA%BF%E5%A4%84%E7%90%86%E5%99%A8/6.6%E6%8E%A7%E5%88%B6%E5%86%92%E9%99%A9%E7%9A%84%E5%A4%84%E7%90%86/%E8%BD%AC%E7%A7%BB%E6%8C%87%E4%BB%A4%E5%AF%B9%E6%B5%81%E6%B0%B4%E7%BA%BF%E7%9A%84%E5%BD%B1%E5%93%8D1.png
  1. 假设在T1的这个时钟周期去取指的就是这条add指令,那么到了第二周期,取指部件取回来add指令,并交给译码部件进行译码,与此同时,取指部件开始取下一条指令,也就是这条sub指令。然后到T3周期, 接着取下一条指令,就是这条beq指令。那通过这段程序我们可以看出,这里很可能会有一段循环, 但这只是从我们旁观者的视角,我们能看到所有的程序代码,而对于处理器来说,现在正处于T3这个周期时,它正在去取下一条指令,它根本不知道这条指令是什么。
  2. 当T3这个周期的取指工作完成之后,虽然这条beq指令的指令编码被取回,也是在T4周期这条beq指令被送到译码部件, 而取指部件则会依次去取下一条指令,那就是这条load指令。当取回这条load指令的时候,beq指令译码也已经完成,但我们仍然不知道它的转移条件是否满足,也就是s3、s4这两个寄存器是否相等,所以这时候我们只能继续取指,那再往下取回的就是这条store指令,就是当T5这个时钟周期完成的时候,beq这条指令也完成了执行的工作,也就是比较完成S3和S4这两个寄存器的值,这时我们才能知道是否要发生这次转移。
  3. 假设转移的条件是满足的,那这样已经进入流水线的这条loadstore指令,实际上是不应该被执行的, 那我们只能把它清除,然后重新从正确的地址开始取指,也就是取回这条减法指令,然后再依次取到这条条件转移指令。但是现在我们所购到处理器并不能记住刚才曾从某个地方取回了这条条件转移指令,所以在这时,处理器只是简单地去取指令,因此它会仍然继续往下取指,再会取到这条load指令,然后再取到一条store指令,只有当这条store指令被取回的时候,刚才取到的这条beq指令才会执行完成,那处理器可能又发现原来是要发生转移的,那必须把loadstore指令清除掉,然后重新取指,那么就发现在这个循环的执行过程中,总是反复地执行了 两条正确的指令,然后取回了两条不应该被执行的指令。

/images/Computer_Organization/6%E6%B5%81%E6%B0%B4%E7%BA%BF%E5%A4%84%E7%90%86%E5%99%A8/6.6%E6%8E%A7%E5%88%B6%E5%86%92%E9%99%A9%E7%9A%84%E5%A4%84%E7%90%86/%E8%BD%AC%E7%A7%BB%E6%8C%87%E4%BB%A4%E5%AF%B9%E6%B5%81%E6%B0%B4%E7%BA%BF%E7%9A%84%E5%BD%B1%E5%93%8D2.png
  • 通过对大量程序的分析可以看出,大约每隔4到7条指令就会有一条转移指令,转移指令所占的比例大约为15%-25%,而且转移指令往往会导致若干条不应该被执行的指令进入流水线, 而清除这些指令则会带来时钟周期的损失,那我们把转移指令所占的比例乘上转移指令带来的时钟 周期的损失,就可以大致地测算出转移指令对性能的影响。

/images/Computer_Organization/6%E6%B5%81%E6%B0%B4%E7%BA%BF%E5%A4%84%E7%90%86%E5%99%A8/6.6%E6%8E%A7%E5%88%B6%E5%86%92%E9%99%A9%E7%9A%84%E5%A4%84%E7%90%86/%E8%B6%85%E6%A0%87%E9%87%8F%E5%A4%84%E7%90%86%E5%99%A8%E5%AE%9E%E4%BE%8B.png
  • 对于比较简单的流水线来说,转移指令带来的损失可能还不大,但是我们知道现代的处理器都是超标量深度流水的处理器,一旦出现转移指令,就有可能导致其后的几十条指令都是不应该被执行的,所以说,流水线越深,超标量数越多,转移指令带来的影响就越大。

/images/Computer_Organization/6%E6%B5%81%E6%B0%B4%E7%BA%BF%E5%A4%84%E7%90%86%E5%99%A8/6.6%E6%8E%A7%E5%88%B6%E5%86%92%E9%99%A9%E7%9A%84%E5%A4%84%E7%90%86/%E8%BD%AC%E7%A7%BB%E5%BC%80%E9%94%80.png
  1. 在执行转移指令的时候,如果确实发生转移,那就需要将其后按顺序预取进入流水线的这些指令废除,也被称为“排空流水线”,然后从转移目标地址重新获取指令。
  2. 一是要判断要不要转移,也就是转移的条件是否成立,如果执行了一条转移指令,但实际不需要发生转移,那刚才按顺序进入流水线的指令就不需要被废除。第二个问题是转移到哪里, 也就是我们为生成目标地址所需要做的工作,那想要消除转移指令带来的影响,我们就要对每一条转移指令都解决这两个问题。

/images/Computer_Organization/6%E6%B5%81%E6%B0%B4%E7%BA%BF%E5%A4%84%E7%90%86%E5%99%A8/6.6%E6%8E%A7%E5%88%B6%E5%86%92%E9%99%A9%E7%9A%84%E5%A4%84%E7%90%86/%E8%BD%AC%E7%A7%BB%E6%8C%87%E4%BB%A4%E7%9A%84%E5%88%86%E7%B1%BB.png
转移指令的分类
/images/Computer_Organization/6%E6%B5%81%E6%B0%B4%E7%BA%BF%E5%A4%84%E7%90%86%E5%99%A8/6.6%E6%8E%A7%E5%88%B6%E5%86%92%E9%99%A9%E7%9A%84%E5%A4%84%E7%90%86/%E6%97%A0%E6%9D%A1%E4%BB%B6%E7%9B%B4%E6%8E%A5%E8%BD%AC%E7%A7%BB1.png
  1. 在MIPS当中,这是一条j型指令,对于这条指令,我们不用判断要不要转移,我们只需要考虑转移到哪里这个问题。
  2. 首先这条指令的编码当中,带有一个26位的立即数,这个数就是要转移的目标地址的主体部分, 但是我们的目标地址应该是32位的,所以还差6位,在差的6位当中,低两位我们用0补上,因为目标地址肯定是四字节对齐的,地址的低两位肯定是0,然后还缺4位,我们通过当前的PC寄存器计算而得。先将PC寄存器的内容加4,得到的这个32位数,取其高4位,和26位地址以及最低的两位的0连接起来,构成了一个32位的数,这就是转移的目标地址。
  3. 在这个目标地址的计算方法只与两个内容有关,1是当前PC的值,2是这条指令本身的编码。

/images/Computer_Organization/6%E6%B5%81%E6%B0%B4%E7%BA%BF%E5%A4%84%E7%90%86%E5%99%A8/6.6%E6%8E%A7%E5%88%B6%E5%86%92%E9%99%A9%E7%9A%84%E5%A4%84%E7%90%86/%E6%97%A0%E6%9D%A1%E4%BB%B6%E7%9B%B4%E6%8E%A5%E8%BD%AC%E7%A7%BB2.png
  1. 当这条j指令处在取指阶段的时候,指令存储器会送出指令的编码,如果我们增加一些简单的电路,就能判断出这是一条j指令。
  2. 同时我们将这条指令编码当中的第26位取出来,在低位加2个0, 然后在这个PC更新的部件当中,已经会完成PC加4的工作,那我们再将这个PC加4的高4位取出,然后拼接而得到一个32位的数,这就是我们要更新的PC的值,也就是这条转移指令的目标地址。
  3. 这些工作都可以在一个时钟周期内完成, 并将这个要更新的PC值送到PC寄存器的输入端,那在下一个时钟上升沿到来的时候,PC寄存器就可以采样到这个要更新的PC的值,那在下一个时钟周期,PC寄存器送出的就是这条转移指令的目标地址了。这样对这条j指令来说,它所需要的转移目标地址在取指阶段就可以获得, 流水线不用停顿。

/images/Computer_Organization/6%E6%B5%81%E6%B0%B4%E7%BA%BF%E5%A4%84%E7%90%86%E5%99%A8/6.6%E6%8E%A7%E5%88%B6%E5%86%92%E9%99%A9%E7%9A%84%E5%A4%84%E7%90%86/%E6%97%A0%E6%9D%A1%E4%BB%B6%E9%97%B4%E6%8E%A5%E8%BD%AC%E7%A7%BB1.png
  • 这条指令是一条R型指令,它的转移目标地址的计算方法是用指令编码当中的rs域指定一个寄存器的编号,用这个编号从寄存器堆当中,取出对应寄存器的内容。

/images/Computer_Organization/6%E6%B5%81%E6%B0%B4%E7%BA%BF%E5%A4%84%E7%90%86%E5%99%A8/6.6%E6%8E%A7%E5%88%B6%E5%86%92%E9%99%A9%E7%9A%84%E5%A4%84%E7%90%86/%E6%97%A0%E6%9D%A1%E4%BB%B6%E9%97%B4%E6%8E%A5%E8%BD%AC%E7%A7%BB2.png
  1. 因为这是间接转移,所以在取指阶段得到指令编码之后,并不能获得转移的目标地址,因此取指部件至少要等待一个周期。那当这条JR指令进入到译码阶段后,指令编码当中的 rs域就会送到寄存器堆,然后得到对应的寄存器的内容。
  2. 如果我们在这里把busA这个信号连接到PC的更新部件,那在JR这条指令的译码阶段结束的时候,转移的目标地址就可以送到PC 寄存器的输入端了。当下一个时钟上升沿来临的时候,这个地址就可以存到PC寄存器当中去,然后在下一个时钟周期,送到指令存储器。
  3. 因此对于这条指令来说,因为我们在译码阶段才能获得转移目标地址,所以流水线需要停顿一个周期。

/images/Computer_Organization/6%E6%B5%81%E6%B0%B4%E7%BA%BF%E5%A4%84%E7%90%86%E5%99%A8/6.6%E6%8E%A7%E5%88%B6%E5%86%92%E9%99%A9%E7%9A%84%E5%A4%84%E7%90%86/%E6%9D%A1%E4%BB%B6%E8%BD%AC%E7%A7%BB1.png
  1. 条件转移指令,它是一条I型指令,这条指令目标地址的计算方法是这样的。
  2. 首先比较rs和rt所指向的寄存器的内容,如果它们相等,它们目标地址是在指令编码当中的16位立即数,进行符号扩展,然后乘以4, 再加上当前PC的内容,再加4,而如果这两个寄存器的比较结果是不相等,那新的PC的值就只是当前PC值加4,那不管寄存器比较的结果是否相等,那这个新的PC的值都只跟当前的PC值 和指令编码的内容相关,而这两项内容在取指阶段都是可以确定的。所以这么看来,目标地址的生成不会造成流水线的停顿, 而问题在于,是否要转移。

/images/Computer_Organization/6%E6%B5%81%E6%B0%B4%E7%BA%BF%E5%A4%84%E7%90%86%E5%99%A8/6.6%E6%8E%A7%E5%88%B6%E5%86%92%E9%99%A9%E7%9A%84%E5%A4%84%E7%90%86/%E6%9D%A1%E4%BB%B6%E8%BD%AC%E7%A7%BB2.png
  1. 因为要判定转移是否成立,需要比较两个寄存器的内容, 而寄存器的内容,我们只能在译码阶段才能获得, 这样与刚才的间接转移类似,我们也得让流水线停顿一个周期,才可以获得这两个寄存器的内容。
  2. 但是与刚才间接指令不同的是,即使到译码阶段的结束,我们依然不能知道转移的条件是否成立,因为我们还需要到执行阶段,将ALU来对这两个数进行比较,从而得到比较的结果。所以在这个结构下,我们需要让流水线停顿 两个周期,才能知道转移条件的判定结果。

/images/Computer_Organization/6%E6%B5%81%E6%B0%B4%E7%BA%BF%E5%A4%84%E7%90%86%E5%99%A8/6.6%E6%8E%A7%E5%88%B6%E5%86%92%E9%99%A9%E7%9A%84%E5%A4%84%E7%90%86/%E6%9D%A1%E4%BB%B6%E8%BD%AC%E7%A7%BB3.png
  • 其实要等到执行阶段结束,无非是要对两个32位数进行比较,而比较两个数相等是一个非常简单的功能,不需要用到ALU这么复杂的部件, 那我们就可以在译码阶段进行一些小的改造。我们在寄存器堆的输出,busA和busB这两个信号给它连接一个额外的比较电路,这个电路是很简单的,速度也很快,不至于影响整个译码阶段的时间。那我们把比较的结果再送到PC的更新部件,那这样在译码阶段 结束的时候,我们就可以将下一条指令的地址送到PC寄存器了。那经过这样的改动,条件转移指令也只需要让流水线停顿一个周期, 就可以让指令正确地执行了。

/images/Computer_Organization/6%E6%B5%81%E6%B0%B4%E7%BA%BF%E5%A4%84%E7%90%86%E5%99%A8/6.6%E6%8E%A7%E5%88%B6%E5%86%92%E9%99%A9%E7%9A%84%E5%A4%84%E7%90%86/%E6%8E%A7%E5%88%B6%E5%86%92%E9%99%A9%E7%9A%84%E5%BD%B1%E5%93%8D.png
控制冒险的影响
/images/Computer_Organization/6%E6%B5%81%E6%B0%B4%E7%BA%BF%E5%A4%84%E7%90%86%E5%99%A8/6.6%E6%8E%A7%E5%88%B6%E5%86%92%E9%99%A9%E7%9A%84%E5%A4%84%E7%90%86/%E5%BB%B6%E8%BF%9F%E8%BD%AC%E7%A7%BB%E6%8A%80%E6%9C%AF.png
  1. 按照通常的规则,这些指令依次进入流水线执行,当执行到这条beq指令的时候,如果t1、t2两个寄存器的内容相同,就会跳到Next所指向的地方,在beq进入流水线之后,必须还需要再等一个周期,才能知道转移条件是否满足,那流水线必须停顿一个周期,那我们现在就是想办法把这个浪费的周期重新利用起来。
  2. 既然我们从硬件上现在无法解决这个问题,那我们不妨就修改这指令行为的定义,我们就规定,它之后的那条指令是一定会被执行的,如果是这样,流水线中就不会出现被浪费的那个周期了。
  3. 还要注意,这样的修改不应该改变程序本来想要达到的结果,所以我们就需要修改一下这段代码,我们要在这个beq指令之后填上一条一定会被执行的指令,那我们只能往上走,但是之前的这条减法指令和加法指令,它们的运算结果正好是beq指令所要比较的这两个寄存器, 所以这条加法指令和减法指令必须在beq指令之前执行。
  4. 再往上看,这条异或指令与我们的判定条件没有关系,现在我们就把这条异或指令挪到beq指令之后,因为我们现在已经修改了转移指令的定义,那我们在流水线的硬件结构上,就可以确定地将beq之后的这条指令进入流水线, 而当这条异或指令完成取指进入译码阶段的时候,这条beq指令的条件判断也已经完成。如果条件成立,这时候就可以从Next所指向的这个地方开始取下一条指令了,否则也可以顺序地取下一条令,但不论是哪一种情况,流水线都不会发生停顿。