有些朋友可能对目前苹果的ROB数据比较模糊,我总结一下目前数据有A站的630slot, dougall测的nop ~2300slot, 还有些写的是~330slot。那么这些数据到底哪一个是对的?其实要解答这个问题我们要先看苹果的ROB设计。苹果在Firestorm/Avalanche设计了用了一个7slot/rob row的设计,每个周期最少释放单位是一个row。但是所有会导致处理器flush pipeline的指令(比如分支指令(分支预测错误),store/load(load dependence预测错误,导致load后面的指令要重新执行 或者 访问地址出现异常))必须要占用每一行的最后一个slot。这些特殊指令导致了在真实运行的代码里往往达不到测试ROB size所得出的大小,举个例子:
add xn, xm
mov xn, xm
store [add], xn
这里虽然只有3条指令但是却需要占用7个rot slot也就是一行。这里store指令相当于起到了一个分隔符的作用。最坏的情况下如果每条指令都要占用一行(比如连续的load,那么rob大小会限制在320(firestorm)。
接下来我们谈谈History FIle,或者说PRRT(Physical register reclaim table),这名词都代表了一个东西。
我们知道在现代乱序执行处理器中,为了解决WAW, WAR,这类anti-dependence同时保持RAW这类real-dependence,都采用了寄存器重命名技术,在苹果的implementation中又一个逻辑寄存器到物理寄存器的mapping table,每当新指令到达rename stage的时候都会通过把自己source operand的逻辑寄存器通过查询mapping table替换成物理寄存器,然后为自己的destination operand申请一个新的物理寄存器,并且更新对应逻辑寄存器到物理寄存器的mapping。可是如果处理器在未来发生了异常、missprediction那么就需要刷新流水线回到异常发生时的状态。可是由于处理器是乱序执行的,mapping table的映射在每一个rename cycle都会被改变。那么如何回到异常发生时那一个周期mapping table的状态?很显然我们需要记录它的状态,这里苹果采用的是记录每一条指令对mapping table所做出的改变(保存在这条指令rename之前 mapping table对应的逻辑寄存器所映射的物理寄存器)。当发生异常时暂停最新的指令执行,从最新的指令所记录的上一个mapping开始往回走恢复到上一条指令的映射 并且释放当前指令的物理寄存器,一直到发生异常的指令。下面给个例子
Inst1: X1 <- P4, Old P1
Inst2: X2 <- P5, Old P2
Inst3: X3 <- P6, Old P3
Inst4: X1 <- P7, Old P4
这里Xn <- Pn代表了这些这条指令会对mapping table做出的改变,Old Pn代表了执行这条指令之前逻辑寄存器Xn所映射到的物理寄存器。
假设Inst1发生了异常我们需要从inst4开始回滚,具体操作是释放当前指令的物理寄存器并且把mapping table改变到执行这条指令之前此逻辑寄存器所map到的物理寄存器。这样一路回滚就可以把mapping table改回到之前的状态,从而继续从发生异常的位置开始执行。
那么知道完理论后,我们知道要实现回滚我们需要记录每条指令对mapping table做出改变之前的状态,History FIle就是用来做这个的,理论上我们是可以把它当成ROB的一个记录项,但是对于很多没有修改物理寄存器的指令来说这就是一种浪费,所以在苹果的实现里,他是单独的一个表(在苹果的专利里叫History Table)。m1这个表的大小是~630这也就对应了A站的数据。
具体测试方法是适用一条不需要占用物理寄存器但是会更改mapping的指令作为ROB的filler指令,比如zero-cycle mov, mov x5, 0。
我们前面说到ROB最少释放单位是7slot/1 row, 根据maynard Handley的数据每周期可以retire 56条nop指令也就是8row。
图源(https://github.com/name99-org/AArch64-Explore Volume 1)
而History Buffer由于涉及到改变Mapping table,释放的速度就会慢一些16slot/cycle(同时释放物理寄存器的数据也是16/cycle)
最后,我想说的是现代商用高性能处理器在不同的层面都做了大量的优化,很多具体实现的技巧课本是都不会说的,想要更深入的了解还是要多读paper,多看看Henry Wong等一系列研究处理器微架构的人的博客。引用Maynard Handley的一句话 “当你读完Hennesy & Patterson 的量化研究的时候你要意识到你仅仅完成了你路途中的10%, 不是90%”。
关于苹果处理器还有很多值得讲的部分比如如何处理指令间的依赖,然后处理OoO内存访问,但是碍于篇幅限制就先写到这里。有机会再继续探讨。
add xn, xm
mov xn, xm
store [add], xn
这里虽然只有3条指令但是却需要占用7个rot slot也就是一行。这里store指令相当于起到了一个分隔符的作用。最坏的情况下如果每条指令都要占用一行(比如连续的load,那么rob大小会限制在320(firestorm)。
接下来我们谈谈History FIle,或者说PRRT(Physical register reclaim table),这名词都代表了一个东西。
我们知道在现代乱序执行处理器中,为了解决WAW, WAR,这类anti-dependence同时保持RAW这类real-dependence,都采用了寄存器重命名技术,在苹果的implementation中又一个逻辑寄存器到物理寄存器的mapping table,每当新指令到达rename stage的时候都会通过把自己source operand的逻辑寄存器通过查询mapping table替换成物理寄存器,然后为自己的destination operand申请一个新的物理寄存器,并且更新对应逻辑寄存器到物理寄存器的mapping。可是如果处理器在未来发生了异常、missprediction那么就需要刷新流水线回到异常发生时的状态。可是由于处理器是乱序执行的,mapping table的映射在每一个rename cycle都会被改变。那么如何回到异常发生时那一个周期mapping table的状态?很显然我们需要记录它的状态,这里苹果采用的是记录每一条指令对mapping table所做出的改变(保存在这条指令rename之前 mapping table对应的逻辑寄存器所映射的物理寄存器)。当发生异常时暂停最新的指令执行,从最新的指令所记录的上一个mapping开始往回走恢复到上一条指令的映射 并且释放当前指令的物理寄存器,一直到发生异常的指令。下面给个例子
Inst1: X1 <- P4, Old P1
Inst2: X2 <- P5, Old P2
Inst3: X3 <- P6, Old P3
Inst4: X1 <- P7, Old P4
这里Xn <- Pn代表了这些这条指令会对mapping table做出的改变,Old Pn代表了执行这条指令之前逻辑寄存器Xn所映射到的物理寄存器。
假设Inst1发生了异常我们需要从inst4开始回滚,具体操作是释放当前指令的物理寄存器并且把mapping table改变到执行这条指令之前此逻辑寄存器所map到的物理寄存器。这样一路回滚就可以把mapping table改回到之前的状态,从而继续从发生异常的位置开始执行。
那么知道完理论后,我们知道要实现回滚我们需要记录每条指令对mapping table做出改变之前的状态,History FIle就是用来做这个的,理论上我们是可以把它当成ROB的一个记录项,但是对于很多没有修改物理寄存器的指令来说这就是一种浪费,所以在苹果的实现里,他是单独的一个表(在苹果的专利里叫History Table)。m1这个表的大小是~630这也就对应了A站的数据。
具体测试方法是适用一条不需要占用物理寄存器但是会更改mapping的指令作为ROB的filler指令,比如zero-cycle mov, mov x5, 0。
我们前面说到ROB最少释放单位是7slot/1 row, 根据maynard Handley的数据每周期可以retire 56条nop指令也就是8row。
图源(https://github.com/name99-org/AArch64-Explore Volume 1)
而History Buffer由于涉及到改变Mapping table,释放的速度就会慢一些16slot/cycle(同时释放物理寄存器的数据也是16/cycle)
最后,我想说的是现代商用高性能处理器在不同的层面都做了大量的优化,很多具体实现的技巧课本是都不会说的,想要更深入的了解还是要多读paper,多看看Henry Wong等一系列研究处理器微架构的人的博客。引用Maynard Handley的一句话 “当你读完Hennesy & Patterson 的量化研究的时候你要意识到你仅仅完成了你路途中的10%, 不是90%”。
关于苹果处理器还有很多值得讲的部分比如如何处理指令间的依赖,然后处理OoO内存访问,但是碍于篇幅限制就先写到这里。有机会再继续探讨。