寄存器分配是关键过程,旨在将高层编译中使用的无限数量的虚拟寄存器映射到硬件提供的有限物理存储资源上,例如x86通用寄存器(GPRs)和浮点寄存器(FPRs)。为了高效完成此任务,编译器必须首先进行“活跃度分析”,以确定一个值确切在何时是“活跃的”,以及何时可以安全地丢弃或覆盖。

活跃度分析的机制

该分析通过遍历代码的基本块来观察虚拟寄存器被定义或使用的每一个点。在提供的实现中,编译器遍历各个块,首先访问终止符以查看所有最终的使用情况,然后反向访问指令以捕获定义和中间使用情况。

对于每个虚拟寄存器,系统维护一个(lo, hi)位置的元组。“lo”位置在每个使用点更新,而“hi”位置仅在最后一个使用点设置一次。这为程序中的每个值创建了一个清晰的“活跃度”窗口。

显示虚拟寄存器及其对应生命周期范围的表格
一个展示定义各种虚拟寄存器活跃度的“lo”和“hi”位置的表格。xnacly.me

此表格直观地表示活跃集,展示编译器如何追踪值活跃范围的起始和结束点。

“寄存器分配是将无限数量的虚拟寄存器分配到硬件拥有的有限存储资源中,具体包括通用寄存器(GPRs)、浮点寄存器(FPRs)和栈。”

— An ode to livelyness analysis; SSA to registers

将SSA与寄存器映射相结合

该项目使用静态单赋值(SSA)形式,因为它简化了分析,因为每个虚拟寄存器(记作%vN)仅被赋值一次。与某些使用phi节点的中间表示不同,此特定实现使用块参数和终止符来促进块间数据流。

通过计算活跃集(livelinetset),编译器可以执行预分配步骤。这允许后端决定一个值是可以在硬件寄存器中驻留,还是必须放置在栈槽中,从而有效地抽象了在降低过程中溢出和恢复值的复杂性。

实现与权衡

开发者展示了将二进制操作降低为字节码的过程,并使用Rust宏来处理加法、乘法和比较等不同类型的指令。这一过程确保即使是复杂的运算也能根据预先计算的活跃集映射到正确的寄存器或栈位置。

展示编译器上下文中的园艺示例的代码列表
一个展示编译器如何处理指令和寄存器分配的实际示例代码列表。xnacly.me

这段代码片段提供了处理指令和管理寄存器映射逻辑的具体示例。

该系统的一个主要权衡是“溢出”的管理。当活跃虚拟寄存器的数量超过可用的硬件寄存器时,编译器必须将值移动到栈中。活跃度分析正是让这一过程变得可管理的关键,因为它告诉编译器哪些值可以安全移动而不会破坏程序的逻辑。