内联缓存()是部分编程语言的运行时系统采用的优化技术,最早为Smalltalk开发是一个常量寄存器加载,后跟一个调用指令。“未初始化(uninitialized)”状态称为“未链接(unlinked)”更佳。寄存器加载了消息选择器(通常是某个对象的地址),而调用是查找当前接收器的类中消息的一个例程,采用上面提过的一级方法查找缓存。然后,运行时例程重写指令,改变载入指令以载入具有当前接收器类型的寄存器,以及调用指令以调用目标方法的前导代码,将调用点“链接”到目标方法。随后继续立即执行前导代码。前导代码将导出当前接收器的类型,并与寄存器中的类型比较;如果认可接收器为同一类型,则继续执行该方法。如果不是,则前导代码再次调用运行时,并可能采用各种策略,其中一种是重新链接新的接收器类型的调用点。
The performance gains come from having to do one type comparison, instead of at least a type comparison and a selector comparison for the first-level method lookup cache, and from using a direct call (which will benefit from instruction prefetch and pipe-lining) as opposed to the indirect call in a method-lookup or a vtable dispatch.
单态内联缓存
如果一个特定调用点经常看到不同类型的对象,内联缓存的性能优势很容易被调用点状态的频繁变化引起的开销所抵消。以下示例构成单态内联缓存的最坏情况:
var values = [1, "a", 2, "b", 3, "c", 4, "d"];
for (var i in values) {
document.write(values[i].toString());
}
同样,方法toString在一个无法事先预知类型的对象上调用。更重要的是,对象类型会随着周遭循环的每一次迭代而改变。单态内联缓存的天真实现会因此不断在“未初始化”与“单态”状态之间转换。为了防止这种情况发生,单态内联缓存的大多数实现支持第三种状态,通常称为“复态(megamorphic)”状态。当某个特定调用点看到预定数量的不同类型时则会进入该状态。一旦某个调用点进入“复态”状态,它就会像“未初始化”状态那样表现,除了它不会再进入“单态”状态(某些单态内联缓存的实现会在一段时间或者执行一次完整的垃圾回收周期后将“复态”调用点改回“未初始化”状态)。
多态内联缓存
为更好地处理经常看到有限数量不同类型的调用点,一些语言运行时采用称为多态内联缓存(polymorphic inline caching)的技术。通过多态内联缓存,一旦处于其“单态”状态的调用点看到第二种类型,它不会回转到“未初始化”状态,而是切换到称为“多态”的新状态。多态调用点根据当前呈递的类型决定调用已知、有限的某一种方法。换句话说,通过多态内联缓存可以在同一个调用点记录多个方法查找结果。由于程序中的每个调用点都可能会看到系统中的每个类型,因此每个调用点上的记录上限通常是查找结果的上限。一旦达到上限,调用点就变成“复态(megamorphic)”并且不再执行更多内联缓存。
典范实现显示,在大型Smalltalk程序中,所有活动方法的发送点有大约1/3维持未链接,其余2/3中,90%为单态(monomorphic),9%为多态(polymorphic),1%(准确来说0.9%)为复态(megamorphic)。
参见
- 记忆化(memoization)
参考资料
外部链接
- [http://www.mirandabanda.org/cogblog/2011/03/01/build-me-a-jit-as-fast-as-you-can/ Article on Inline Caching in the Cog Smalltalk VM]
评论 (0)