GEP-10


元数据
编号

GEP-10

标题

静态编译

版本

4

类型

功能

状态

最终

评论

在 Groovy 2.0 中实现,并在后续版本中增强

负责人

Cédric Champeau

创建日期

2011-11-23

最后修改日期 

2018-10-26

摘要:静态编译

此 GEP 在该语言中引入了名为静态编译的实验性新功能。当不需要 Groovy 的动态特性并且动态运行时的性能过低时,可以使用静态编译。读者必须理解,

  • 我们不打算改变常规 Groovy 的语义

  • 我们希望静态编译能够显式触发

  • 我们希望静态 Groovy 的语义尽可能接近动态 Groovy 的语义

基本原理:静态类型检查与静态编译

静态编译严重依赖于另一个名为静态类型检查的功能,但重要的是要理解它们是独立的步骤。你可以对代码进行静态类型检查,但仍然可以使用动态运行时。静态类型检查在编译过程中添加类型推断。这意味着,例如,方法调用的类型参数是推断出来的,闭包或方法的返回类型也是推断出来的。这些推断出来的类型被检查器用来验证程序的类型流。在这样做时,必须意识到静态检查器不能像动态 Groovy 一样运行。这意味着,即使程序通过静态类型检查,它也可能在运行时表现出不同的行为。

方法选择

动态 Groovy 中的方法选择

Groovy 支持多方法。特别是在动态 Groovy 中,目标方法是在运行时选择的,与在编译时选择目标方法的 Java 形成对比。让我们用一个例子来说明差异

public void foo(String arg) { System.out.println("String"); }
public void foo(Object o) { System.out.println("Object"); }

Object o = "The type of o at runtime is String";
foo(o);

在 Java 中,这段代码将输出 "Object",因为目标方法是根据编译时参数的类型选择的。 "o" 的声明类型是 Object,所以选择 Object 版本的方法。如果你想调用 String 版本,你必须将 "o" 定义为 "String",或者在方法调用参数中将 o 强制转换为 string。现在是动态 Groovy 中的同一个例子

void foo(String arg) { println 'String' }
void foo(Object o) { println 'Object' }

def o = 'The type of o at runtime is String'
foo(o)

如果你运行这段代码,你会看到 Groovy 打印 "String"。原因是 Groovy 在运行时解析类型参数,并在存在多个版本的情况下选择方法签名的最专业化版本。为了处理多个参数,Groovy 计算实际类型参数和方法声明的参数类型之间的距离。选择得分最高的那个方法。

方法选择过程很复杂,使 Groovy 非常强大。例如,还可以定义元类,它们将选择不同的方法,或动态更改方法的返回值类型。这种行为是 Groovy 调用时间与 Java 相比性能较差的主要原因。然而,随着 Groovy 中引入了调用站点缓存,性能得到了极大提升。即使有了这些优化,Groovy 的性能也远不及纯静态语言。

InvokeDynamic

InvokeDynamic 支持正在开发中,应该会在即将发布的 Groovy 2.0 中引入。InvokeDynamic 允许我们做的基本上是将调用站点缓存替换为 JVM 直接实现的本机 "动态方法分派" 系统。我们还不能确定会达到什么性能提升。在最好的情况下,我们可能接近静态类型语言的性能。然而,由于某些功能仍然难以用 InvokeDynamic 实现,因此性能提升可能不会那么高。重要的是要谈论它,因为

  • invokedynamic 允许 JIT 优化

  • 替换当前的动态方法分派,而不会改变语言的语义

  • 如果性能良好,可能使静态编译变得不必要

然而,InvokeDynamic 存在一个主要缺点:它只适用于使用 Java 7+ JVM 的人。如果我们想为我们的用户提供 Java 般的 Groovy 程序性能,我们能否负担得起让大多数用户没有这种性能?很有可能在两三年内,Java 7 不会成为主流。

然而,Rémi Forax 为旧版本的 JVM 创建了一个 invokedynamic 的移植版本。这种移植版本依赖于字节码转换,将 invokedynamic 指令替换为 "模拟指令"。这种 "模拟" 模式对我们 Groovy 开发人员来说很棒,因为它允许我们编写一次代码,并在任何 JVM 上运行它,但对于用户来说,性能很可能很差。为了确保这一点,一旦 invokedynamic 支持在 Groovy 核心实现,就会进行一个使用移植版本 InDy 的实验。然而,最可能的情况是,这种代码的性能仍然远不及静态语言所能提供的性能。

静态 Groovy

基于类型推断的分派

此 GEP 的目的是在 Groovy 中试验静态编译模式。通过我们之前所解释的,你应该已经明白静态类型代码意味着与动态代码不同的行为。如果你期望静态检查和静态编译的代码与动态 Groovy 的行为完全一致,那么你应该停止在这里,或者等待 invoke dynamic 支持,以期获得更好的性能。如果你完全理解静态编译的代码意味着不同的语义,那么你可以继续阅读此 GEP,并帮助我们选择最佳的实现路径。实际上,这里有几个选项,我们将在这里解释。

当前的实现依赖于静态类型检查器,它执行类型推断。这意味着,对于前面的例子

void foo(String arg) { println 'String' }
void foo(Object o) { println 'Object' }

def o = 'The type of o at runtime is String'
foo(o)

编译器能够推断出当调用 foo 方法时,实际类型参数将是一个字符串。如果我们对它进行静态编译,则此静态编译程序在运行时的行为将与动态 Groovy 相同。对于这样的实现,我们期望大多数程序在静态情况下表现得像它们在动态 Groovy 中那样。然而,这永远不可能完全一样。这就是为什么我们说这种行为 "尽可能接近" 动态 Groovy 的行为。

这种实现的缺点是开发人员无法轻松地知道编译器选择了哪个方法。例如,让我们看看这个例子,它来自于邮件列表上的讨论

void foo(String msg) { println msg }
void foo(Object msg) { println 'Object' }

def doIt = {x ->
  Object o = x
  foo(o)
}

def getXXX() { "return String" }

def o2=getXXX()
doIt o2   // "String method" or "Object method"????

静态类型检查器从 getXXX 的返回值类型推断出 o2 的类型,因此知道 doIt 是用 String 调用的,因此你可能会怀疑程序会选择 foo(String) 方法。然而,doIt 是一个闭包,因此它可以在很多地方重复使用,其 "x" 参数的类型是未知的。类型检查器不会为使用它的不同调用站点生成不同的闭包类。这意味着当你处于闭包中时, 'x' 的类型是闭包参数中声明的类型。这意味着如果没有 'x' 的类型信息,x 被认为是一个 Object,将被静态选择的 foo 方法将是具有 Object 参数的那个方法。

虽然这可能会令人惊讶,但这并不难理解。为了正确运行,你必须要么在闭包中添加显式类型参数,这是始终首选的。一句话,在静态检查的世界中,最好限制类型将被推断的地方,以便代码能被正确理解。即使你不这样做,修复代码也很容易,所以我们认为这不是一个重大问题。

Java 般的方法分派

另一种实现方法不是依赖于推断的类型,而是完全像 Java 一样运行。这样做主要的好处是,用户不需要理解 3 种不同的方法分派模式,就像在前面的解决方案中那样(Java、动态 Groovy、推断的静态 Groovy)。主要的缺点是静态 Groovy 的语义并不接近动态 Groovy 的语义。因此,这不是首选的实验性实现。如果你认为应该优先考虑此版本,请不要犹豫,发送电子邮件到邮件列表,以便我们进行讨论。你甚至可以 fork 当前的实现来提供自己的实现。

测试

静态编译现在是 Groovy 2.0.0 版本的一部分。你可以下载最新的 Groovy 2 版本并进行测试。

@CompileStatic

你可以尝试以下代码片段

@groovy.transform.CompileStatic
int fib(int i) {
    i < 2 ? 1 : fib(i - 2) + fib(i - 1)
}

这段代码应该已经可以像 Java 一样快地运行了。

用于直接方法调用的 "箭头" 运算符

一些用户提出另一个与静态编译相关的想法,即 "箭头运算符"。基本上,这个想法是引入另一种在 Groovy 中调用方法的方式

obj.method() // dynamic dispatch
obj->method() // static dispatch

虽然这个想法听起来很有趣,特别是当你想要在一个方法中混合动态代码和静态代码时,但我们认为它有很多缺点。首先,它引入了语法更改,这是我们尽可能想避免的。其次,该运算符背后的理念是在你 **知道** 对象类型时执行直接方法调用。但是,如果没有类型推断,你就有两个问题

  • 即使 'obj' 的类型已指定,你也不能确定在运行时类型是否相同

  • 你还必须推断参数类型,这让我们面临着之前相同的难题:依赖于类型推断,或者依赖于 Java 般行为,其中方法是根据声明的类型选择的。如果我们这样做,那么我们就会引入与静态模式的潜在不兼容性...... 所以我们必须在这两种模式和完整的静态编译模式之间做出选择。

想象一下以下代码

void write(PrintWriter out) {
   out->write('Hello')
   out->write(template())
}

def template() { new MarkupBuilder().html { p('Hello') } }

虽然第一个调用可以轻松解析,但第二个调用就不是这样了。你可能不得不这样重写你的代码才能让它工作

void write(PrintWriter out) {
   out->println('Hello')
   String content = template() // dynamic call, implicit coercion to string
   out->println(content) // static method dispatch based on declared types only
}

def template() { new MarkupBuilder().html { p('Hello') } }

如果你使用 @CompileStatic 注解,这是不必要的

@CompileStatic
void write(PrintWriter out) {
   out.println('Hello')
   out.println(template())
}

def template() { new MarkupBuilder().html { p('Hello') } }

邮件列表讨论

更新历史

3 (2012-06-21)

从 Codehaus wiki 中提取的版本

4 (2018-10-26)

许多小调整