你了解CPU吗?(五)

1.写在前面

前面我们已经介绍了CPU的一些基础的信息,以及如何构建一个数据通路,以及如何构建CPU的流水线,以及CPU流水线会带来那些问题,但是在历史的长河里面,CPU做到这些还远远不够,还需要更快,也是一直奋斗的目标。今天我们会讲剩下的部分,还有一种比较重要的控制冒险,就是CPU的中断,同时会介绍一些新的机制来加速CPU的流水线。

2.中断(例外)

控制逻辑是处理器设计中最有挑战的部分:验证正确性最为困难,同时也最难进行时序优化。exceptioninterrupt是控制逻辑需要实现的任务之一。除分支指令外,它是另一种改变指令执行控制流的方式。最初,人们使用它们是未来处理CPU内部的意外事件。后续经过可处理与CPU进行通信的IO设备。

在X86中就是使用interrupt。而在RISC-V中exception来指代意外的控制流的变化,而这些变化无须区分产生的原因是来自处理器内部还是外部。使用interrupt仅仅指代由处理器外部事件引发的控制流变化。具体我们可以看如下:

在这里插入图片描述

exception处理的许多功能需求来自引发例外的特定的场合。检测和处理exception的控制逻辑会处于处理器的时序关键路径上,这对处理器时钟频率和性能都会产生影响,如果对控制逻辑中的exception处理不给于充分重视,一旦尝试在复杂设计中添加exception处理,将会明显降低处理器的性能。这和处理验证一样复杂。

2.1RISC-V体系结构中如何处理exception

我们这儿只说两种exception:未定义指令和硬件故障。如果在执行某条指令的时候,发生硬件的故障,这个时候在系统例外程序计数器(SPEC)中保存发生例外的指令地址,同时将控制权转交给操作系统。

之后,操作系统将做出相应动作,包括为用户程序提供系统服务,硬件故障时执行预先定义好的操作,或者停止当前程序的执行并报告错误。完成例外处理所有操作后,操作系统使用SEPC寄存器中的内容重启程序的正常执行。可能是继续执行原程序,也可能是终止程序。

操作系统进行例外处理,除了引发例外的指令外,还必须获得例外发生的原因。目前使用两种方法来通知操作系统。

RISC-V中使用的方法是设置系统例外原因存储器,该寄存器中记录了例外原因。

另外一种方法是使用向量式中断。该方法用基址寄存器加上例外原因(作为偏移)作未目标地址来完成控制流转换。

基址寄存器中保存了向量式中断内存区域的起始地址

在这里插入图片描述

操作系统可根据例外向量起始地址来确定例外原因。如果不使用这种方法,如RISC-V,就需要为所有例外提供统一的入口地址,由操作系统解析状态寄存器来确定例外原因。对于使用向量式例外的设计者,每个例外入口需要提供比如32字节或8条指令大小的区域,供操作系统记录例外原因并进行简单处理。

通过添加一些额外寄存器和控制信息,并稍微扩展控制逻辑,就可以完成对各种例外的处理。假设,我们使用统一入口地址的方式实现例外处理,设置地址为0000 0000 1C09 0000。我们还需要添加两个额外的寄存器。

  • SEPC:64位寄存器,用来保存引起例外的指令的地址
  • SCAUSE:用来记录例外原因的寄存器。在RISC-V体系结构中,该寄存器为64位,大多数位未被使用。假设对上述提及的两种例外类型进行编码并记录,其中未定义指令的编码为2,硬件故障的编码为12.

2.2流水线实现中的例外

流水线实现中,讲例外处理看成另一种控制冒险。

处理分支预测错误时,我们将取指阶段的指令变为空操作(nop),以此来消除影响。对于进入译码阶段的指令,增加新逻辑控制译码阶段的多选择器输出为0,流水线停顿。添加一个新的控制信号ID.Flush,它与来自于冒险检测单元的stall信号进行或操作。使用该信号对进入译码阶段的指令进行清除。对于进入执行阶段的指令,我们使用一个新的控制信号EX.Flush,使得多选择器输出为0。RISC-V体系结构中使用0000 0000 1C09 0000作为例外入口地址。为保证例外入口地址送到寄存器。于是有了如下的图:

在这里插入图片描述

上述例子我们需要注意一个问题:如果我们在add指令执行完毕后检测例外,程序员讲无法获得x1寄存器中的原值,因为它已更新为add指令的执行结果。如果我们在add指令的EX阶段检测例外,可以使用EX.Flush信号避免该指令在WB阶段更新寄存器。有一些例外类型,需要最终完成引发例外的指令的执行。最简单的方法就是清除该指令,并在例外处理结束后从该指令重新开始执行。

最后一步是,在SEPC寄存器中保存引发例外的指令的地址。

同时还有一个问题,对于流水线处理器,每个周期同时有5条指令在流水线中执行,例外处理的挑战在于如何将各种例外与指令进行对应。而且,同一周期内可以同时发生多个例外。解决方法是,对例外进行优先级排列,便于判断服务顺序。

3.指令间的并行性

流水线技术挖掘了指令间潜在的并行性,这种并行性被称为**指令级并行(ILP)**提高指令级并行度主要有两种方法。第一种是增加流水线的级数,让更多的指令重叠执行另外一种提高指令并行度的方法是,增加流水线内部的功能件数量,这样可以每周期发出多条指令。这种技术被称为多发射

实现多发射处理器主要有两种方法,区别在于编译器和硬件的不同分工。如果指令发射与否的判断在编译时完成的,称为静态多发射如果指令发射与否的判断是在动态指令过程中由硬件完成的,称为动态多发射

在多发射流水线中,需要处理处理如下两个主要任务:

  • 将指令打包并放入发射槽,在大多数静态发射处理器中,编译器会完成这部分工作。而在动态发射器中,这部分工作通常会在运行时由硬件自动完成,编译器可以通过指令调度来提高发射效率。
  • 处理数据和控制冒险。在静态发射处理器中,编译器静态处理了部分或所有指令序列中存在的数据和控制冒险。相应的,大多数动态发射处理器在执行过程中使用硬件技术来解决部分或所有类型的冒险。

3.1推测的概念

推测是另外一种非常重要的深度挖掘指令级并行的方法。以预测思想为基准,推测方法允许编译器或处理器来猜测指令的行为,并允许其他与被推测指令相关的指令提前开始执行。

推测的难点在于预测结果可能出现错误。因此,所有推测机制都必须包括预测结果正确性的检查机制,以及预测出错后的恢复机制,以消除推测式执行带来的影响。这种恢复机制的实现增加了结构设计的复杂度。

可以在编译时完成推测,也可以在执行时由硬件完成推测。

实现推测错误时的恢复机制非常困难。在软件实现的推测中,编译器经常需要插入额外的指令来检测的正确性,并在检测到推测错误时提供例程进行恢复。在硬件推测式执行中,处理器通常会保存推测的结果直到推测被确定是正确的。如果推测式正确的,将使用保存的推测结果更新寄存器或存储器,完成推测路径上的指令如果推测式错误的,硬件清除推测结果,并从正确的指令处重新开始执行,推测错误需要对流水线进行恢复或者停顿,这显然会极大地降低性能

推测式执行还会引入另一个问题:**对某条指令进行推测还可能引入不必要的例外。**比如某条load指令处于推测式执行,同时该load指令的访存地址发生了越界,则会引发例外。如果推测式错误的,这就意味着发生了本不该发生的例外。如果推测是错误的,这就意味着发生了本不该发生的例外。这个问题非常复杂,因为如果这条load指令不是推测执行,那么例外时一定会发生的。对于编译支持的推测式执行,可以通过添加特定支持来避免这样的问题,对此类例外一直延迟响应直到确认推测正确。对于硬件推测式执行,例外将被记录直到确认推测正确,这时被推测的指令将被提交,检测到例外,转入正常的例外处理程序进行执行。

如果推测正确,处理器的性能将被改善;一旦推测错误,处理器的性能会受到较大影响。

3.2静态多发射

静态多发射处理器是由编译器支持打包和处理指令间的冒险。对于静态多发射处理器,可以将同一周期发射出去的指令集合(发射指令包)看成一条需要进行多种操作的"大指令"因为静态多发射处理器通常会对同一周期发生的指令类型进行限制,将发射指令包看成一条预先定义好、需要进行多种操作的指令,这正符合超长指令字的设计思路。

同时,大多数静态发射处理器也依赖编译器来处理数据和控制冒险。编译器的任务包括静态分支预测和代码调度,以减少或消除所有的冒险。

举个例子:

为了解静态多发射技术,我们考察一个简单的双发射RISC-V处理器。其中,指令序列中的一条指令是定点ALU指令或者分支指令,另一条指令是load或者store指令。通常,一些嵌入式处理器正是如此来使用。单个周期内发射两条指令需要同时取指和译码64位指令。在许多静态发射处理器,特别是超长指令字处理器中,为简化指令的译码和发射,对可同时发射的指令组合做出了限制。例如:需要指令成对,指令地址需要64位边界对齐,ALU指令和分支指令放在前面。而且,如果指令对中的一条指令无法发射,需要将其替换成nop指令。这样一来,就保证了指令总是成对发射,当然其中一条可能是nop。

在这里插入图片描述

静态多发射处理器对于潜在的数据和控制冒险有不同的解决方法。在一些设计实现中,由编译器来实现所有冒险的解决、代码的调度以及插入相应的Nop。因此在代码动态执行过程中,硬件可以完全不去关心冒险检测或者流水线停顿的产生。

另外一些设计实现中,使用硬件来检测两个指令包之间的数据冒险,并产生相应的流水线停顿。编译器只负责在单个指令包中检测所有类型的相关。即便如此,单个冒险也通常会导致整个指令包的发射停顿。不论是采用软件来解决所有的冒险,还是仅在两个指令包间降低冒险发生的比例,如果使用上文中提到的"单条大指令"的思想来进行分析,将更有助于加深理解。

如果想同时发射ALU和数据传输类指令,除了上文所说的冒险检测和流水线停顿逻辑,首先需要添加的硬件资源是寄存器堆的读写口。在同一个时钟周期内,ALU指令需要读取两个源寄存器,store指令可能需要读取两个以上的源寄存器,ALU指令需要更新一个目标就寄存器,load指令也需要更新一个目标寄存器。由于ALU部件只负责ALU指令的执行,因此还需要额外增加一个加法器来进行访问地址的计算。如果不增加这些额外的硬件资源,我们的双发射流水线将产生大量的结构冒险。

很明显,这种双发射处理器最多提高两倍的性能,但这也需要程序中存在两倍的、可重叠执行的指令数目。而这种重叠执行又会因增加数据和控制冒险而导致性能损失。为什么会导致性能的损失?具体的如下,例如,在我们的简单五级流水线结构中,load指令有一个周期的使用延迟。如果下一条指令需要使用load指令的结果,那么它必须停顿一周期。同样在双发射五级流水线结构中,load指令也存在一个周期的使用延迟,而这时需要停顿后续两条指令的执行。而且,在单发射五级流水线中,ALU指令本来是没有使用延迟的。但在双发射流水线中,需要同时发射ALU指令和load或store指令。如果这两条指令存在数据冒险,则load或store指令不能被发射,相当于ALU指令增加了一个周期的使用延迟。为有效挖掘多发射处理器中可用的并行性,需要使用更高级的编译器或硬件动态调度技术,静态多发射器对编译器提出了更高的要求。

在这里插入图片描述

举个简单例子,具体指令如下:

Loop:
	ld x31,0(x20) // x31 = array element
	add x31,x31,x21 // add scalar in x21
	sd x31,0(x20) // store result
	addi x20,x20,-8 // decrement pointer
	blt x22,x20,Loop // compare to loop limit
					// branch if x20 > x22

于是有了如下的指令的图,具体的如下:

在这里插入图片描述

于是有了新的优化的方式,就是循环展开是一种专门针对循环体提高程序性能的重要编译技术。它将循环体展开多遍,从不同循环中训中可以重叠执行的指令来挖掘更多的指令级并行性。于是有了如下的执行的流程,具体的如下:

在这里插入图片描述

在循环展开的过程中,编辑器使用了额外的寄存器(x28、x29和x30),这样的过程称为寄存器重命名。寄存器重命名的目标是,处理真数据相关,消除指令间存在的其他数据相关。这些数据相关将会导致潜在的冒险,或者妨碍编译器进行灵活的代码调度。如果只使用x31,考虑展开后的代码将会如何:ld x31,0(x20),add x31,x31,x21,之后跟着sd x31,8(x20),这样的指令序列不断重复,除了都使用x31,这些指令实际上是相互独立的。也就说说,不同循环的指令之间是没有数据依赖的。这种情况称为反相关或名字相关。

在循环展开时对寄存器进行重命名,可以允许编译器移动不同循环中的指令,以更好地调度代码。重命名的过程可以消除名字相关,但不能消除真相关。

3.3动态多发射处理器

动态多发射处理器也称为超标量处理器或朴素的超标量处理器。在最简单的超标量处理器中,指令按序发射,由硬件来判断当前发射的指令数:一条还是更多,或者停顿发射。显然,如果想让这样的处理器获得更好的性能,仍然需要编译器进行指令调度,消除指令间的相关,提高指令的发射率。不过,即使编译器配合进行了指令调度,在这个简单的超标量处理器和超长指令字处理器之间仍然存在一个重要的差别,即不论软件调度与否,硬件必须保证代码运行的正确性。此外,编译生成代码的运行正确性应该与发射率或处理器的流水线结构无关。但是,在一些超长指令字处理器中,情况却不一样。代码需要重新编译才能正确运行在不同处理器实现上。还有一些静态多发射处理器,虽然代码在不同的处理器实现上应该能运行正确,但实际情况经常会比较糟糕,仍然可能需要编译器的支持。

许多超标量处理器扩展了动态发射逻辑的基础框架,形成了动态流水线调度技术。动态流水调度技术由硬件逻辑选择当前周期内执行的指令,并尽量避免流水线的冒险和停顿。

动态调度流水线

动态调度流水线由硬件选择后续执行的指令,并对指令进行重排来避免流水线的停顿。在这样的处理器中,流水线被分成三个主要部分:取指和发射单元,多功能部件以及提交单元。具体的如下图:

在这里插入图片描述

取指和发射单元负责取指令、译码、将各指令发送到相应的功能单元上执行。每一个功能单元前都有若干缓冲区,称为保留站。保留站中存放指令的操作和所需的操作数。只要缓冲区中指令所需操作数准备好,并且功能单元就绪,就可以执行指令。一旦指令执行结束,结果将被传送给保留站中正在等待使用该结果的指令,同时也传送到提交单元中进行保存。提交单元中保存了已完成指令的执行结果,并在指令真正提交时才使用它们更新寄存器或者写入内存。这些位于提交单元的缓冲区,通常被称为重排序缓冲。和静态调度流水线中的前递逻辑一样,重排序缓冲也可以用来为其他指令提供操作数。一旦指令提交,寄存器得到更新,就和正常流水线一样直接从寄存器获取最新的数据。

我们先来看如下的步骤:

  1. 发射指令时,指令会被拷贝到相应功能单元的保留站中。同时,如果指令所需的操作数已准备好,也会从寄存器堆或者重排序缓冲中拷贝到保留站中。指令会一直保存在保留站中,直到所需的操作数已被拷贝至保留站中,它们在寄存器堆中的副本就无须保存了,如果出现相应寄存器的写操作,那么该寄存器中的数值将被更新。
  2. 如果操作数不在寄存器堆或者重排序缓冲中,那它一定在等待某个功能单元的计算结果。该功能单元的名字将被记录。当最终结果计算完毕,将会直接从功能单元拷贝到等待该结果的保留站中,旁路了寄存器堆。

上面这些步骤充分利用了重排序缓冲和保留站来实现寄存器重命名。

从概念上讲,可以把动态调度流水线看作程序的数据流结构分析。处理器在不违背程序原有的数据流顺序的前提下以某种顺序执行指令,被称为乱序执行。这是因为这样执行的指令顺序和取指的顺序是不同的。

为使得程序行为与简单的按序单发射流水线一致,乱序执行流水的取指和译码都需要按序进行,以便正确处理指令间的相关。同样,提交阶段也需要按照取指的顺序依次将指令执行的结果写入寄存器和存储中。这种保守的处理方法称为按序提交。如果发生例外,处理器很容易就能找到例外前的最后一条指令,也会保证只更新在此之前的指令需要改写的寄存器。虽然流水线的前端和后端都是按序执行,但是功能部件是允许乱序执行的。任何时候只需要数据准备好,指令就可以被发射到功能部件上开始执行,目前,所有动态调度的流水线都是按序提交的。

更为高级的动态调度技术还包括基于硬件的推测式执行,特别是基于分支预测。通过预测分支指令的转移方向,动态调度处理器能够沿着预测路径不间断地取指和执行指令。由于指令都是按序提交的,在预测路径上的指令提交之前就已经知道分支指令是否预测成功。支持推测执行的动态调度流水线还可以支持load指令访存地址的推测。这将允许乱序执行load store指令,并使用提交单元来避免不正确的推测。

3.4高级流水线和能效

通过动态发射和推测式执行深度挖掘指令级并行能力也会带来负面影响,其中最重要的就是降低了处理器的能效。每一个技术上的创新都可能产生新的结构,使用更多的晶体管来获取更高的性能。但是这种做法可能很低效。目前,我们已经撞上了功耗墙,因此转向设计单芯片多处理器架构,这样就无须像之前那样设计更深的流水线或者采用更激进的推测机制。

我们相信,虽然简单处理器运行速度不如复杂处理器,但是相同的性能下它们的能耗更低。因此,当结构设计受限于能量而非晶体管数量时,简单处理器能够在单芯片上获得更高的性能。具体的如下图:

在这里插入图片描述

4.写在最后

通过这几章的简单的介绍,我们大概的理解了CPU的执行的流程,简单的数据通路,以及CPU如何通过流水线来提高吞吐量,同时流水线带来了那些问题,以及我们是如何解决的,同时最后还简单介绍两种的发射器,一种是静态发射器,还有另外一种动态发射器。

U如何通过流水线来提高吞吐量,同时流水线带来了那些问题,以及我们是如何解决的,同时最后还简单介绍两种的发射器,一种是静态发射器,还有另外一种动态发射器。

本图文内容来源于网友网络收集整理提供,作为学习参考使用,版权属于原作者。
THE END
分享
二维码
< <上一篇
 

)">
下一篇>>