LLVM 3.0中的寄存器贪婪分配

原文地址:http://blog.llvm.org/2011/09/greedy-register-allocation-in-llvm-30.html

LLVM有两个新的分配器:基本与快速。在LLVM 3.0发布时,缺省的优化寄存器分配器将不再是线性扫描,而是新的贪婪寄存器分配器。使用全局生命期分解,这个贪婪算法生成的代码比线性扫描要小1-2%,快至10%。

线性扫描的经验教训

自2004年,线性扫描就是LLVM的缺省寄存器分配器。这样一个简单的算法工作得出奇地出色。事实上,简单的设计使得调整算法以获得生成代码的细小的提升变得更容易。更先进的寄存器分配算法通常需要构建昂贵的数据结构,或者它们假设生命期是不变的。这使得,比如,在线交换一个两地址指令,或者重新施行一个常量池载入而不是溅出到栈,变得困难。

新的寄存器分配算法必须保持这个简单性。它必须是在算法运行时可以改变机器代码。线性扫描依赖寄存器重写器,在寄存器分配之后,清理代码。理论上,重写器只应该将虚拟寄存器重写为分配的物理寄存器,不过它知道许多其他伎俩。当线性扫描做了一些蠢事,比如从一处栈槽两次载入寄存器时,重写器将重用第一个寄存器,并且消除第二次载入来清理代码。这个算法是局部的,它不能清理跨越单个基本块的混乱局面。重写器总是通过消除明显的错误来挽救局面。尽管它的代价高昂。它大约占据了线性扫描的一半编译时间,并且它大量的伎俩使得代码发出难以维护。

新的寄存器分配器应该避免犯明显的错误,这样重写器可以专注在重写寄存器上。

正如名字暗示的,线性扫描通过以一个线性的次序访问生命期来工作。它维护在函数当前点存活的生命期的一个活动列表,这就是它检测冲突,而无需计算整个冲突图的方式。活动列表是线性扫描速度的关键,但它也有其最大的弱点。

在所有物理寄存器被活动列表中的冲突生命期所阻塞时,一个生命期被选中溅出。要溅出的生命期没有首先分解会导致重写器难以清理的混乱。我们宁可将它们分解为可分配的更小的片段,但这将要求线性扫描回溯。这非常耗时,完全的生命期分解对线性扫描实际不可行。

基本分配器

 

新的基本分配器去除了线性扫描对线性次序访问生命期的依赖性。相反,它使用一个优先级队列以溅出权重的降序访问生命期。用于冲突检查的活动列表被一组生命间隔联合(liveinterval union)替代。实现为每个物理寄存器的一棵B+树,它们是检查与已分配生命期冲突的高效方式。不像活动列表,生命间隔联合可以任何优先级队列序工作。

当一个生命期不能被分配到其寄存器类别中的任何物理寄存器时,它被溅出。因为生命期以溅出权重的降序被分配,所有在这个生命间隔联合中的冲突生命期都具有更高的溅出权重。无需查找更好的溅出候选。

在CISC架构上,溅出槽内存访问通常可以被折叠入现存的指令。在RISC架构上,必须插入显式的载入与保存指令。这还将在溅出代码与使用该被溅出生命期的原始指令间创建新的小生命期。这些新生命期被以一个无限大的溅出权重加回优先级队列——它们不能被再次溅出。

在技术上,这些具有高溅出权重的新生命期应该被首先分配,但基本分配器从不回溯。因此,会发生这样一个生命期被已经分配的具有更小溅出权重的生命期所阻塞。在这个情形下,分配器选择一个物理寄存器,并溅出已经分配给该寄存器的冲突生命期。

基本分配器产生非常类似于线性扫描的代码,它也依赖于虚拟寄存器重写器清理代码来得到好的结果。它没有明显超出线性扫描,它主要是为了测试优先级队列与生命间隔联合框架。基本算法非常简单,它提供了使用伎俩的许多机会。贪婪算法就是这样做。

贪婪分配器

 

 

 

关于基本算法首先要注意的是,对于最优地着色寄存器,它的优先级队列不能很好地工作。溅出权重计算为使用密度,小的生命期倾向于具有高的溅出权重。这意味着所有小的生命期被首先分配。它们用光了寄存器类别中的优先寄存器,大的生命期开始争抢余下寄存器。它们的大多数以溅出结束。

贪婪算法通过首先分配大的生命期避免了这个问题。这使得所有的寄存器类别对大的生命期可用,而小生命期通常可以放入空隙里。一些函数有太多大的生命期,没有足够的空间给所有的小生命期。溅出带有高溅出权重的小生命期确实很糟,因此从生命间隔联合中逐出具有更低溅出权重的已分配的生命期。被逐出的生命期被剥夺它们的物理寄存器,并放回优先级队列。它们将在在别的地方获得再次被分配的机会,或者它们会继续进行生命期分解。

当一个生命期找不到允许逐出的冲突生命期时,它不是立即溅出。如果可能,它被分解为更小的片段,并放回优先级队列。这是一个非常重要的优化。一个大的生命期可能在许多时间都是不活动的,但在一个热的循环里被大量使用。通过创建覆盖这个热循环的独立生命期,它有很大机会分配到一个寄存器。余下的生命期可以在循环外溅出,在那里它是不活动的。一个生命期仅在分解器确定分解没有帮助时溅出。这通常发生在所有繁忙区域都被隔离后,并且余下的生命期仅包含至及自这些繁忙区域的几个拷贝。

生命期分解与逐出间的交互创建了一个逐渐改进的过程。随着生命期围绕繁忙区域分解,它们得到更高的溅出权重。这使得它们可以逐出在该区域不那么忙碌的更旧的生命期。被逐出的生命期被分解,以此类推。

分解的渐进过程通常在生命期变小前终止,结果是一组生命期覆盖多条指令,或甚至多个基本块。这意味着没有什么东西留给重写器清理,而且的确贪婪算法使用了一个完全平凡的只有85行的重写器,相比之下旧的重写器有2600行。

由贪婪算法生成的代码几乎总是优于线性扫描。通常这是因为生命期分解能够从循环消除溅出代码。虽然贪婪算法确实知道更多一些伎俩。

伎俩

使算法尽可能灵活,避免引入任意限制是一个重要的设计目标。在任何时候改变机器码及生命期是可能的。简单地逐出相关的生命期,做出修改,将它们放回优先级队列。这个灵活性允许寄存器分配器使用许多伎俩:

·        寄存器偏好。函数参数由ABI定义在指定的物理寄存器里传递。LLVM通过函数调用前后物理与虚拟寄存器之间的拷贝来表示。寄存器分配器尝试将虚拟寄存器分配给相同的物理寄存器,使得拷贝可以被消除。线性扫描对此从不擅长——偏好的寄存器通常被之前的分配所占据。

·        偏好更小的编码。在类似ARM Thumb2及x86-64架构上,一些寄存器要求更长的指令编码。在分配高代价的寄存器之前,贪婪算法将从低代价寄存器中逐出不那么重要的生命期。这意味着更长的指令编码更少得到使用,代码的整体大小减少。

·        死代码消除。类似重物化的优化导致生命期更短,或甚至完全无用。贪婪算法将精确地重新计算生命期,并递归地消除死代码。

·        寄存器类别膨胀。生命期分解创建了被更少指令使用的虚拟寄存器。这有时会解除限制,使虚拟寄存器可以被移到一个更大的寄存器类别。依赖于架构,这使对新生命期可用的寄存器数目加倍。

贪婪寄存器分配器仍然有许多提升的空间。这是它取代线性扫描的全部意义。

 

  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值