上节课学习了指令集架构,其实就是软件和硬件的接口,分为两类,即缩减集和指令集。在复集的发展过程中,逐渐吸收了约简集的思想。虽然外部输入是复杂的集合系统接口,但内部会分解成微指令。流水线技术的设计思想是单周期处理器:一个指令周期内只能执行一条指令。单周期处理器六个阶段串行工作,某一时刻只有一部分硬件工作,硬件利用率低。为此,我们采用了流水线技术。流水线不仅仅是一种思维,更是一种处理器加工结构。按照这种思想设计的处理器结构称为流水线结构。以洗车为例,了解流水线的思想。洗车相当于处理设备和汽车相当于指令。当洗车场只有一个人时,相当于一个单循环处理器。如果三个人同时洗车,就相当于并行计算机中的多核处理器。因为洗车过程中有些工序相互干扰,比如不能同时洗外洗和洗内洗,所以所花的时间并不会因为涉及的人数多而成倍减少。这种方法称为完全并行。如果采用流水线作业,洗车分为三个工序,每个人只负责一个工序。假设洗一辆车需要30分钟,从微观(单车)来看,全部并行至少需要10分钟(在三个人互不干扰的前提下),而在流水线责任至少30分钟,不包括转移工序的时间;但从宏观上看(车源源不断),一辆车只需要10分钟。因此,在设计处理器时,我们采用组合逻辑电路来优化单周期处理器。组合逻辑电路分为运算和存储部分,时钟周期不能小于运算和存储时间之和(300+20=320ps)。我们利用流水线思想进一步优化,将计算部分分为三个阶段,上一阶段的输出等于下一阶段的输入,并在每一阶段之后插入存储部分,存储上一阶段的计算结果在存储部分阶段。当时钟周期处于上升沿时,数据可以存储在存储单元中并传递给下一级。我们以上面提到的三级流水线为例。一条指令从执行到完成需要3个时钟周期。内存虽然有时间开销(20ps),但时钟周期只有120ps。这样,从微观上看,一条指令的执行需要360ps,也就是300多ps;但是在进行流水线操作时,一个时钟周期完成一条指令只需要120ps,速度更快。而且在一个时间点只有一部分组件完全并行工作,流水线连续工作,提高了硬件的利用率。因此,流水线就是将一个时钟周期内只能执行一条指令变为多个时钟周期内可以执行一条指令。因此,时钟周期减少并且处理速度增加。管道设计中遇到的挑战并不是平均分配的。在实际的处理器设计中,很难将处理器一分为三。因为每个部分的功能不同,所以不能保证每个部分都用同样的时间完成功能。时钟周期必须保证每个部件都能正确完成结果的计算,以最慢的部分为准。因此,提高最慢部分的速度对于提高整体性能最为关键。较快的部分无关紧要。管道划分的越细,收益越高。如果所有部分平分的话,性能确实会不断提升,但是后期性价比会下降。如果将三级再细分为六级,则需要在中间插入五个储能电路。从单周期到第3阶段,性能提升一倍以上;但从第三阶段到第六阶段,也只是翻了一倍多而已。所以,性能确实是提升了,但是提升会越来越慢。而且有效计算时间比例会越来越低,存储时间开销会越来越大(引入了太多的存储组件)。随着存储元件越来越多,成本越来越高,单条指令的执行时间越来越长。因此,我们必须有一个权衡折衷的概念:在成本和性能之间找到一个平衡点。如果各部分不均分,则性能提升可能不会得到提升。它需要看哪一部分被进一步细分。如果是最慢的部分,性能会提高;如果是快的部分,性能不会提高。一个通用的流水线处理器包括五个阶段:取地址、译码、执行、内存访问和回写。与指令执行的六个阶段不同,程序计数器的更新被合并到地址获取中。因为取地址和更新程序计数器的计算量很小,即使结合起来,还是比最慢的阶段要快。执行和内存访问是最慢的阶段。因为执行逻辑比较复杂,内存访问需要访问内存,比较慢。后来,更复杂的执行和内存访问阶段被进一步划分以缩短时钟周期。现代处理器设计中遇到的挑战:理想情况下指令执行是非常罕见的数据依赖问题指令之间具有数据依赖性。前面一条指令运算得到的值会用在后面的指令中,这在单周期处理器中完全没有问题,但是在多周期处理器中,如果第一条指令还没有执行完,第二条指令需要执行。在使用数据的阶段,就会出现问题。这个问题有两种解决方案。一种是在指令中插入延迟。例如,如果指令1的计算结果用于指令2的B阶段,则在b阶段之前插入一个气泡,使处理器空一拍(所有指令都延迟一拍),让指令1完成了。也就是说,一些并行指令是通过插入气泡来序列化的。但是,如果管道中插入的气泡过多,性能会下降,这种情况很常见。第二种是现代处理器常用的解决方案:乱序执行。在不影响流水线性能的前提下,打乱执行顺序(但不改变最终逻辑)。指令顺序的重排由硬件完成,将指令加载到指令池中,分析依赖关系,重新整理顺序,然后送入流水线,使其满负荷运行。控制依赖问题流水线执行时,如果有跳转指令,很难说后面会加载哪条指令。只有计算出跳转指令的结果,即执行完跳转指令后,才能知道执行哪条指令。如果发生跳转,则需要丢弃原来后面执行的指令,恢复引起变化的指令。这也导致管道排空和效率降低。目前主要用分支预测的方法来解决这类问题,即猜测会不会有跳转,或者跳到哪里。动态分支预测:根据经验猜测。比如for循环在某条指令处第一次跳转到某个地址,然后处理器会在执行到那条指令时自动跳转到那个地址,这样最后只会出现两种错误,一种是第一次,还有一次是最后一次。在现代处理器中,动态分支预测技术的成功率已经达到90%以上。联想到rep:ret,rep是一个空指令,什么都不做,只是因为前面有一个跳转指令,两条跳转指令不能连续使用,所以用rep空指令隔开。这是由于动态分支预测的局限性,无法预测两个连续的跳跃目标。因此,虽然插入了空指令,但是提高了效率。尽管浪费了一个滴答,但静态分支预测避免了流水线刷新:不是由处理器,而是由编译器。例如,gcc编译器中有一个内置函数。根据可能性决定在跳转指令后面紧跟哪个分支,使得下面执行的指令是逻辑执行的指令,也可以通过条件数据传输来解决,即先计算未来的发展趋势处理器超标量技术。将执行阶段细分,设计了四个算术运算组件,还设计了一些数据加载和数据存储组件。相当于把执行阶段看成多个并行,前面的寻址阶段是串行的。这样就实现了执行阶段的并行化,从宏观上缩短了单条指令的执行时间。好比银行先排队取票,然后在多个窗口服务。也就是说,在单核处理器中,一些本地指令可以完全并行化。
