跳转至

往期经验

常制不可以待变化,一涂不可以应无方,刻船不可以索遗剑。——《抱朴子·广譬》

本文档旨在总结Zircon 2023开发过程的反思与经验总结,以供Zircon 2024开发参考。

流水线控制信号:养虎遗患

流水线控制信号,指的是那些需要跨多个流水级来控制段间寄存器的信号,例如flush、stall。这些信号在开发初期很有可能会忽视它们的危害,而导致开发者无节制地使用它们接到流水线寄存器上,从而导致了以下问题:

  • 线延迟:随着电路面积的增大,控制信号需要走过的路程越来越长,如果对这些信号的使用不加节制,这会导致它的线延迟越来越大;例如对于DCache Miss信号,如果它本身生成逻辑就较为复杂,且需要接入多个模块,那么就可能导致布线困难,从而导致线延迟增大;
  • 扇出:这类信号如果不加控制,在接入多个模块时,会导致删除过大。例如提交段的flush信号需要接入流水线内所有段间寄存器。

解决以上两个问题的方法是很显然的,只不过需要在开发初期对这些控制信号进行严格的考虑和限制:

  • 尽可能让这些信号从寄存器中直接接出:此方法可以可观的减少生成逻辑的延迟,从而使得组合门的负载变小,让时序有足够的余量来满足线延迟。
  • 复制多个寄存器降低扇出:降低扇出的常规方法,减小扇出后布线也会变得更加容易。

缓存系统:笨鸟先飞

在处理器设计过程中,缓存系统是最容易出现Bug的一部分。当一条store指令很早就写缓存后,如果相隔100000个周期后load这个数据,那么就会出现很多问题:

  • store是否丢包了?是否正确写入写缓存了?
  • store是否被正确提交了?是否真的写入缓存了?
  • 如果需要被换出,那么是否向存储器正确的地址写入了正确的数据?再次调入的时候是否正确?
  • load是否丢包了?

一切的原因根本上源自于store指令的结果是难以用difftest对比的。因为一条store写入的数据一开始在Cache中,可能会在未来某个阶段换出到下一级Cache,可能会在未来某个阶段写入到存储器中——换句话说,store指令的结果所在位置是无法简单地捕捉的(除非我们每个周期都把Cache、内存中的所有数据都和参考核对比,那仿真效率显然是不可接受的)。因此,这个问题的解决方案是:

  • 在整个缓存系统开发完成时,单独对该系统做difftest测试。
  • 测试过程中,要保存内存每个地址最近被store时的周期计数器的值,方便出现bug时进行回溯查找。

队列压缩:压烂逻辑

《超标量处理器设计》书中提到,发射队列的组织有两种方式:

  • “压缩式”,即每当一条指令从队列中被选中离开发射队列后,在它后面的指令要依次前移,从而“压”掉这条发射出去的指令的空位;
  • “非压缩式”,即发射出一条指令后,其余指令的位置不移动。

压缩式发射队列的问题

对于压缩式设计,它的好处就是很容易通过位置来确定指令的新旧,因为靠近队头的指令一定是最旧的指令,最应该被发射出去。同时,它还有一个诱人的好处就是写入逻辑简单,因为只需要用一个写指针跟踪当前队内最旧指令的位置,每次写入写入它后面的位置即可。但是,这样的设计说起来美好,但实际上却存在一系列问题:

  • 每次对队列中的元素进行唤醒操作时,需要先确定当周期是否要发射,然后先组合地移位一下,再对新位置的指令进行唤醒。
  • 写入逻辑同理,需要先判断是否发射,才能确定写入的位置。
  • 如果一个周期要从队列中选出多条指令发射,那么压缩逻辑会变得异常复杂(每个位置的候选值数量和发射的宽度成正比)。

换句话说,对这个队列的一切操作都要先判断是否发射,而发射逻辑还会受到DCache Miss等控制信号的影响,所以和这个队列的功耗和延迟直接可以宣判这种设计的死刑(至少作者没有想到什么比较好的解)。因此,虽然《超标量处理器设计》书中提到非压缩式的设计难度比较高,但真正实现之后是可以解决前面的所有问题的。

非压缩式发射队列

非压缩队列的实现难点在于以下几点:

  • 写入队列时需要找到一个“空位”,作者在这里采用了维护一个空闲位置列表的方法来实现。
  • 发射需要找到“最旧的指令”,这需要一个二叉树比较归约逻辑。
  • 对于load指令需要判断其是否为队列内最旧的指令,因为如果其不为最旧的load,那么uncache时就无法做到保序了,这个信息可以帮助我们重演这条load指令。
  • 回收空闲位置的时机很重要,提交段flush时,如果一股脑全部回收,那么可能需要回收的空闲位置会非常多,从而导致写入逻辑变得非常复杂。

但很显然,这种队列的唤醒、写入逻辑延迟都不再受到发射逻辑延迟的影响,因此即便实现难度较大,但仍然是一个值得尝试的设计。

分支预测:勿以善小而不为

观察性能测试的执行统计可以发现,大部分分支指令属于普通分支指令,函数调用和返回只占据了不到10%。计算机体系结构设计的一个原则是:加速经常性过程,因此看起来提高普通分支指令的预测准确率,是比提升函数调用和返回预测准确率更划算的。

设计原则没有错,问题在于,提升函数调用和返回预测准确率,是会对普通分支指令的预测准确率和其他指标产生影响的。例如,如果函数返回的预测正确率比较低,那么处理器执行路径也会“误入歧途”,导致普通分支预测的历史可能被“污染”(需要更多的时间代价来恢复),ICache也会因为经常到达一个陌生的地址区域,出现命中率下降和数据污染的问题,从而影响IPC(因为ICache的缺失处理是不能被打断的)。

Zircon-2023忽略掉了函数返回预测的及时性对整体性能的重要性,因为Zircon-2023只是很简单地使用一个返回地址栈进行预测,而对于返回地址栈处于错误路径的情况没有多加关注。事实证明,在使用了更加完善的函数返回预测后,Zircon-2024在运行性能测试时(特别是dhrystone时),IPC的提升是相当明显的。

另外,使用r1的值来预测返回地址是一个糟糕的决定(该方法在如《计算机体系结构基础》等多本参考书中都有提及),因为gcc经常会在函数返回时最后一个恢复r1,这会导致这条恢复指令还没有提交时,返回地址就已经被预测出来了。而如果使用更为激进的策略,例如使用rename段的映射表来寻找r1对应哪个物理寄存器,那么经常会导致PC跳转到一个很奇怪的地址,从而导致ICache性能下降。

TO BE CONTINUED