<a>动态优化编译器</a>
我们希望 Java 应用程序的计算部分只涉及 Java 源代码的一小部分。Jalape�o 的优化编译器致力于高效地编译这些字节码。优化编译器是 动态的:它在应用程序运行时编译方法。将来,优化编译器也将是 自适应的:它将在计算密集的方法上被自动调用。优化编译器的目标是在给定的编译时间预算内生成所选定方法的尽可能好的代码。此外,它的优化必须在正确地保护异常、垃圾回收和线程的 Java 语义的同时很大地提高性能。对实现 SMP 服务器的可伸缩性性能来说,降低同步和其它线程原语的花费尤其重要。最后,以最少的工作量把优化编译器的目标重定向到各种硬件平台应是可能的。构建达到这些目标的动态优化编译器是一个很大的挑战。
从字节码到中间表示(intermediate representation)
副本传播是在转换期间执行快速优化的一个示例。Java 字节码常常包含执行计算并把结果存储到本地变量的指令序列。对中间表示的生成的一个天真想法是为计算结果创建一个临时寄存器,并另外用一条指令把这个寄存器的值移入本地变量。简单的副本传播试探法清除了大量这些不必要的临时寄存器。当从临时寄存器把值存储到本地变量时,将检测到最近生成的指令。如果是这条指令创建了这个临时寄存器来存储结果,那么这条指令就被改成把结果直接写到本地变量。
为尽量减少为同块字节码生成 HIR 的次数,简单贪婪算法为抽象解释选择有最低开始字节码索引的块。这个简单的试探法依赖于这样的事实,即除了循环,所有控制流构造都是以拓扑有序方式生成的,而且控制流图是可简化的。偶而地,这个试探法看来获得了用当前 Java 源代码编译器编译的方法的扩展基础块的最优顺序。
高级优化
HIR 中的指令很好地模仿了 Java 字节码,有两个重要不同 ― HIR 指令在符号寄存器操作数上进行操作,而不是在隐式堆栈上,而且 HIR 包含独立的操作符,用于实现运行时异常的显式检查(例如,数组边界检查)。相同的运行时检查通常需要多于一条的指令。(例如,incrementing A[ i] 可能涉及两个独立的数组访问,但只需一次边界检查。)对这些检查指令的优化减少了执行时间并使另外的优化更容易。
当前,HIR 使用带有适度编译时开销的简单的优化算法。这些优化有三类:
本地优化。这些优化对扩展基础块是本地的,例如,公共子表达式清除、冗余异常检查清除以及冗余装入清除。
这种技术能捕捉到很多优化机会,但其它的情况只能由流敏感的算法检测到。
方法调用的内联扩展。为在 HIR 级别上把方法调用扩展成内联,被调用方法的 HIR 被生成并被补入到调用者的 HIR。静态的、基于大小的试探法当前用于控制对静态和最后方法的调用的自动内联扩展。对于非最后虚方法调用,优化编译器预测虚调用的接收方为对象的声明类型。它用运行时测试来监视每个内联虚方法以验证对接收方的预测是正确的,如果不正确,则缺省设置为正常虚方法调用。在有动态类装入的情况下,这种运行时测试是安全的。
由于 Jalape�o 是用 Java 写的,与用于把应用程序方法扩展为内联的框架相同的框架也可用于把对运行时方法的调用扩展为内联(特别是同步和对象分配)。一般说来,从应用程序代码到 Java 库,下至 Jalape�o 运行时系统,都可以把调用扩展为内联,这为优化提供了极好的机会。
低级优化
在执行了高级分析和优化之后,HIR 被转换为 低级中间表示(low-level intermediate representation(LIR))。LIR 把 HIR 指令扩展为特定于 Jalape�o 虚拟机的对象布局和参数传递约定的操作。例如,虚方法调用被表达为类似于 invokevirtual 字节码的单条 HIR 指令。这一条 HIR 指令被转换在三条 LIR 指令,分别负责从一个对象获得 TIB 指针,从 TIB 获得适当方法体的地址,以及将控制转到方法体。
由于字段和头的偏移量现在都是可用的常数,新的优化机会出现了。原则上,任何高级优化也可用在 LIR 上。然而,由于 LIR 的大小可能是相应 HIR 大小的两到三倍,所以在进行 LIR 优化时要更留意编译时间开销。目前,清除本地的公共子表达式是 LIR 上进行的唯一优化。由于 HIR 和 LIR 共享相同的基础设施,所以在 HIR 上执行公共子表达式清除的代码不用修改就可重用在 LIR 上。
指令选择和特定于机器的优化
BURS 是代码生成器(code-generator)生成器,类似于扫描器和分析器生成器。想得到的目标体系结构的指令选择由 树语法(tree grammar)指定。树语法中的每一条规则都有一个相关花费(反映生成的指令的大小和指令的预期周期数)和代码生成动作。处理树语法以生成一组表,这些表在编译时驱动指令选择。
在指令选择上使用 BURS 技术有两个重要好处。首先,编译时进行的树型匹配法通过使用动态编程为所有输入树找到了最少花费的分析(与树语法中指定的花费相比)。其次,构建 BURS 基础设施的花费可在几个目标体系结构中分期付清。特定于体系结构的部分相对较少;Jalape�o 的 PowerPC 树语法约有 300 条规则。
方法序言分配一个堆栈框架,保存方法需要的任何非易失性寄存器,并且检查是否有人提出让出请求。结语恢复任何被保存的寄存器并解除堆栈帧分配。如果方法被同步,则序言锁定,而且结语解锁,指定对象也被同步。
优化编译器然后把可执行的二进制代码放到指令数组,即方法体。通过把中间指令偏移量转换为机器代码偏移量,这个装配阶段也最后确定了异常表和指令数组的引用映射图。
优化的级别
优化编译器可在不同的优化级别上执行。每个级别都包含前一级别的所有优化和一些其它东西。 级别 1恰好包含上面描述的优化。(存在 级别 0 主要是出于调试的目的,它与级别 1 相似,但没有任何高级或低级优化。)两个级别的更加激烈的优化也在计划之中。
操作的形态。
优化编译器的运作方式是想作为自适应 JVM 的一个组件。图 5 显示了这样一个虚拟机的整体设计。优化编译器是 Jalape�o 的自适应优化系统的关键组成部分,这个自适应系统也包含联机测量(on-line measurement)和正在开发的控制器子系统(controller subsystem)。通过使用软件采样和成型技术和来自硬件性能监视器的信息的概要,联机测量子系统将监视单个方法的性能。当联机测量子系统检测到某个性能阈值被达到时,控制器子系统将被调用。控制器将用概要信息构建一个“优化计划”,这个计划描述了哪个方法应被编译以及应用哪一个优化级别。然后调用优化编译器来编译优化计划中的方法。联机测量子系统继续监视单个方法,包括那些已经优化的方法,以在必要时触发进一步的优化过程。
优化编译器也可用作 JIT 编译器,在方法第一次执行时编译所有方法。当要设定优化编译器的性能基准时,优化编译器同时用作静态引导映象编译器(针对引导映象的 JVM 代码)和 JIT 编译器(针对基准程序代码和任何余下的 JVM 代码)。