1.3 让指令飞
Superscalar与ooo(Out-of-order)的引入极大促进了现代处理器微架构的发展。已知的高性能处理器,如Nehalem,Sandy Bridge,Opteron,Power甚至是ARM Cortex系列处理器都使用了这种构架。这类方法在有效提高了ILP(instruction level parallelism)的同时,加大了整个Cache Memory层次结构的实现难度。
在此我们只讨论存储器读写指令在Superscalar与OOO环境下的执行过程。存储器读写指令的执行过程似乎非常简单。即使是只写过几行汇编代码的程序员亦可对此娓娓道来。许多人认为存储器读不过是将数据从主存储器中将数据读入寄存器,存储器写是将寄存器中的数据写入到主存储器中。
这个执行过程很难用一句话回答,即便是将使用的处理器模型进行大规模的约束。在一个支持Superscalar和OOO的处理器中,一条指令的执行被分解为若干步骤。指令首先进入Pipeline的Front-End,包括Fetch与Decode,之后经过Dispatch和Scheduler后进入执行单元,最后Commit执行结果。
假设在一个微架构中,所有指令使用In-Order方式通过Front-End,并采用Out-of-Order方式进行Issue,之后使用Out-of-Order Execution和Completion方式,在最后进行Commitment时使用In-order的方式。其中指令Commitment的定义是在其执行完毕,并将最后结果更新至ROB(re-order buffer)和LSQ(Load-Store Queue)的过程。
现代处理器在Commit最后的执行结果时大多都采用In-order方式,这也保证了指令在经过Out-of-Oder的流水线后,程序员看到的最终结果与程序应有的顺序一致。多数程序员被这一假象迷惑,认为CPU的乱序执行仅与硬件流水线相关,并不会影响软件程序。
事实并非如此。微架构为了实现乱序执行,有些指令,比如存储器读指令,可能会提前执行,而后因为种种原因,如分支预测失败,可能会被迫重新执行。虽然乱序流水线可以保证最后的结果与程序期待的结果一致,但是无法完全抹去这条本不该执行的指令在流水线中,在存储器子系统中留下的执行痕迹。
为了进一步简化模型,我们仅讨论在经过这些约束后的CPU中,存储器读写指令的执行过程。与其他指令相比,这两条指令的执行过程更加显得步履蹒跚。下文以Nehalem微架构为参照说明存储器指令的执行过程。Nehalem微架构Pipeline的组成结构如图1‑3所示。
存储器读写指令在经过Front-End阶段时进行了很多细节处理工作,尤其是对于x86处理器,此处不再对此做进一步的描述。这些存储器读写指令在经过Front-End之后,将首先通过Rename/Allocate部件,使用Renaming技术可以解决与存储器读写最直接相关的WAW,WAR相关,之后等待源Operand准备就绪,并将其Dispatch给Scheduler。这些指令需要从RS(Reservation Stations)获得可用的Entry,对于存储器读写指令还需要从LSQ中预留空间,最后插上ROB Tag的翅膀,经In-Order或者Out-of-Order的发射(Issue)过程,自由飞翔。
这些飞翔的指令无序,而且指令流水线会让最笨的鸟尽可能的提前飞翔。存储器读写指令就是其中最笨的几只鸟。这些飞翔着的存储器读写指令将飞向第一个目的地LSQ,还有一些执行信息会同步到ROB和RS的对应Entry中。
这些飞翔的指令有快有慢,有应该飞的也有不应该飞的。速度不同的指令必须在到达第一个终点LSQ时,按序等待提交。不应该飞的指令必须在最后的Commitment阶段之前被发现,然后被抛弃,重新飞翔。这也造成了在一条指令流水线中存在多个Outstanding的读和写指令,可并行的最大读写指令由ROB,LSQ和RS中的Entry数目决定。在很多现代处理器中,LSQ的Entry数目多小于ROB和RS中的Entry数目,因此在一个Pipeline中可以并发的读写指令首先由LSQ的深度决定。
无序飞翔存储器读写指令在执行过程中,需要尽量无视对方的存在,最大可能的实现飞翔。因此也引入了读写指令的Speculation机制。由于Out-of-Order Issue的原因,后续的指令可能先于之前的指令执行,由于Out-of-Order Execution和Out-of-Completion的原因,率先发射的指令也不一定最先抵达目的地。
这些机制制造了多种乱序的可能。虽然指令的最终结果仍然是In-Order Commitment,但是这一机制并不能保证存储器指令的执行轨迹是in-order的。存储器指令的执行与其执行轨迹的异步引出了一个异常沉重的话题Memory Consistency。这个话题将有专门的篇章讨论。
我们首先分析存储器读写指令的执行过程。这两大类指令都是访问存储器的,虽然一个是进行写,另一个是进行读,但是依然有其相同点。我们首先讨论其相同点。在一个CPU中,读写指令在进入Pipeline之前,首先被分解为两个微步骤或者是两个微指令,这并不是x86处理器所特有的,许多为了提高存储指令执行效率的微架构都使用了这种方式。
其中一条微指令用来计算指令使用的EA。在有些处理器微架构中,每一条Load/Store指令在其之前的Store指令的EA计算完毕后才能发射,这一机制有效解决了存储指令间的RAW类相关,但是这种方式较为Conservative。激进的想法是先做,错了再纠正,只要错误带来的惩罚小于做对了所带来的收益即是一次有效行为。
在现代处理器微架构中,虽然程序员直接使用EA,但是对于存储器读写指令,由于各类寻址方式的引入,Pipeline并不会很顺利地得到最终EA。在x86处理器中,EA的计算公式如公式1‑4所示。
这样的公式显然并不容易很快的计算出结果,这使得现代处理器多设置了AGU(Address Generate Unit)这个专门的执行部件。AGU部件在计算出指令使用的EA后,会将其传递给在LSQ中对应的存储器读写微指令,之后这些微指令才开始真正的存储器操作。
在流水线中指令的执行过程很难用用一句话说清楚。权衡读写指令复杂度后,我们决定先讨论存储器写指令,这条指令的执行过程与读有类似之处,也有很多区别。存储器写指令的第一个目的地是LSQ的对应Entry,这条指令在获得源Operand和EA后,将进行存储器写。在没有描述清楚Cache层次结构之前,我们无法说明存储器写如何在流水线中执行。为未来预留说明空间,我们暂时简化存储器写的执行过程。
任何一个处理器体系结构都会谨慎地处理存储器写指令的执行过程。设计者都明白一个基本道理,如果你向一个指定的存储器写入一个指定的数据后,你很难用常规的手段重新获得其历史信息。写是不能悔棋的,即便其目标是一个Well-Behavior的存储器。这些谨慎创造了存储器写这个胆小鬼。流水线中执行的存储器读和写指令,其胆量的差别是质的,存储器读指令在没有按序轮到其执行之前已经周游了大千世界,而存储器写指令在执行完毕那一瞬间甚至还没有离开过家门。
在多数现代处理器微架构中,存储器写指令只有在最终Commit之后才能真正向Cache层次结构发送数据。这并不意味着存储器写指令不能Speculative,存储器写在Commit之前也如存储器读一样自由,而且在多数情况下写操作也需要首先读取数据,之后进行数据合并,然后进行真正的写操作。存储器写指令在最后的Commit阶段时异常谨慎,只有在确保一定不会出现问题之后才能提交。这一机制保证了存储器写操作在指令流水线中可以按序完成,也导致了存储器写的延时。
对于一个最终存储器类设备,在流水线中执行的存储器写操作所采用的这种方法依然不能保证到达DDR颗粒或者PCIe设备时,存储器写操作依然会按序到达。一次存储器写操作的轨迹很长,一部分在CPU域中,一部分会在设备域。按序Commit与各类Barrier指令只能保证在CPU域的序。当存储器访问到达其他设备域时,需要遵守其他序规则。这些内容超出了在这里的讨论范围。
我们重新讨论存储器写的延时。正如大家所知,存储器写可以Posted,原本可以较快的执行完毕,只是由于Speculative的原因,存储器写操作很有可能会被错误地提前执行,只是在这条存储器写指令没有Commit之前,都有修改的余地。这个余地带来了写的延时。
这个延时在一个追求极致的微架构中是不能忍受的。最具智慧的一群人在有资格一起进行极限挑战时,差距非常微小。在这些人眼中,处理器的一个Cycle已经被细化为十分之一,甚至百分之一。两个追求极致的CPU,指令平均执行效率如果相差了一个Cycle,意味着这两个CPU本不应该在同一个舞台上竞争。
必须要最大程度地利用这个延时,以提高存储器读的效率。与存储器写相比,存储器读非常幸运。在正常情况下,对于Well-Behavior存储器的某个地址读一千次一万次,也不能将其从0读成1。读操作可以更大胆一些。
存储器读指令获得EA后,首先需要访问的是LSQ,检查是否在尚未Commit的写操作中是否具有数据副本,从而消除了存储器写带来的延时。在LSQ中没有命中时,存储器读指令才会访问Cache层次结构,并将结果放入对应的LSQ等待提交。在访问Cache层次结构时,这些读指令有快有慢,这造成了存储器读的乱序。更为糟糕的是,有些读操作可能不应该被提前执行。虽然这个存储器读的结果可以被指令流水线最终丢弃,但其执行轨迹却无法消除。
这是因为Load/Store Speculation,也引出了数据访问序这些概念。很多系统架构师熟悉这些序,也在有意无意的利用着这些序。下文讲述的这个实例发生在在一个多处理器并行编程系统中。在这个系统中,存在C1和C2这两个CPU,并使用共享存储器进行通信。
C1首先向A1地址写入一个命令,之后将A2地址的数据加1。C2使用存储器读操作获得A2的数据后,首先进行数据检查,发现数据变化后,得知A1中已经存放了新的命令,之后再处理这个命令,其源代码示意如图1‑4所示。
这样的代码在支持Load/Store Speculation的微架构中,有很多问题。一个简单的想法是C2的Load Speculation可能导致Read A1得到的命令并不是最新的。但是C2在何种情况之下才能得到旧的数据,并不是简单说说Load Speculation这样容易。
曾经有一个朋友与我说,买卖股票其实很容易,Just buy low and sell high,But how难住了他。Load Speculation引发灾难的各种可能性也很难用一两句话描述清楚。此处仅以图1‑4说明一个简单的可能出现错误的场景。
假定C1没有出现问题,对A1和A2这两个地址的存储器写访问按序进行。与此同时,C2采用Load Speculation技术以加速存储器读,但是Read A2和Read A1这两个操作最终依然按序完成。只是C2在访问A2时,发生了Cache Miss,经过很长一段时间之后,假设这段延时为m,C2才能获得真正的数据,而Speculative Read A1读取的数据在Cache中命中,C2将其放入LSQ中,等待最后的提交。
如果C2最终从A2中的获得数据没有改变(Check A2),则重新读取A2重新判断,在LSQ中的A1将被抛弃。Speculative Read A1的结果会被抛弃,并没有什么问题。只有在某个机缘巧合之下,Speculative Read A1的结果才会错误,这个概率非常低。
假设在延时m这个时间段里,C1更新了A1和A2并对于C2按序到达。只是在C1更新A1之前,C2已经进行完成Speculative Read A1,此时C2得到的是A1的一个历史值。C2在延时m内最终获得了A2,这个值已经被C1更新,因此C2将获得更新后的A2,此时分支预测单元将误认为C2的Speculative Read A1的结果正确,不会重新对A1进行访问。此时造成了C2获得了一个最新的A2和旧的A1,引发了一个致命错误。
有很多方法解决这个错误,只需要在Check A2之后,显示加入Barrier类指令,避免Load Speculation,或者直接设置MMU禁止对这段存储器访问的Load Speculation。虽然只有在某个机缘巧合之下,Speculative Read A1的结果才会错误,而且这个概率非常低,但是我们仍然需要保证这段程序在执行过程中100%正确。问题是我们要为不到1%的概率,将如此沉重的Barrier操作强加到99%以上的每一个正确之上。
这个Barrier并非不可移除。只要C2发现Read A1命中Cache之后,将其之前出现的Read Miss读操作加上Snoop Resync Flag的标志。标志为Snoop Resync Flag的读指令在Commit结果时,抛弃其后的读操作,就能避免这些沉重的Barrier操作。在这个例子中,使用这样机制后,Speculative Read A1的结果将被流水线丢弃,之后重新执行,沉重的Barrier操作得以取消。AMD Opteron使用了这种机制[13],类似的机制也出现在其他现代处理器中。
有心人会从各类处理器的优化手册和这些公司近十年来发表的IP,猜测出这些处理器实现的某些细节,即便这些公司敞帚自珍一些更重要的细节,从不发表这些细节的IP。敞帚自珍总是暂时,时间将冲破任何障碍。美国中央情报局称,早在RSA算法发布之前,他们已经掌握了公钥体制,只是没有公开。他们的孤芳自赏没有改变这个体制的如期而至,只是成就了Rivest,Shamir和Adleman的不世之功,传世之名。
不想继续探讨有关IP的深重话题,但是总有要说的话。IP自有其存在的道理。羊儿有羊儿的道理,狼儿有狼儿的道理。IP以保护知识产权的名义诞生,历史越是悠久的公司,IP的数量也更加多些。在很多时候,我们并不知晓有些IP是如何审批通过。某些行业的某些IP有如“天是蓝的,草是绿的”这样的常识。这些IP不因我们称其无耻而消亡,这些IP的发源地很多是来自某些如雷贯顶的巨型公司。
IP制度最初是为了保护创新,发展至今总能找出其妨碍创新的实例。很多时候真正的创新产生于悲寒交迫饥饿着的人群中,不发生在衣食无忧领取薪水,悠哉游哉喝着咖啡聊着天的白领中。这使得很多历史悠久的公司为真正创新所做出的努力反而不如在车库中工作着的年轻人,这是一个客观存在的事实。
这些年轻人历千辛万苦的成果,却能很轻易地被“天是蓝的,草是绿的”这样的IP拒之门外。深入思考这些问题后剩余是无奈后的反思。天将是蓝的,草依旧是绿的。人类的生存与发展,需要的是不断进取所带来的创新,这是世界大行,是再多的魍魉魑魅,再多的阴谋诡计无法阻挡的。