= GEP-10: 静态编译 :icons: font .元数据 **** [horizontal,options="compact"] *编号*:: 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 = "o 在运行时的类型是 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 = 'o 在运行时的类型是 String' foo(o) ``` 如果您运行此代码片段,您会看到 Groovy 打印 "String"。原因是 Groovy 在运行时解析类型参数,并在可能存在多个版本时选择方法签名中最特化的版本。 为了处理多个参数,Groovy 计算实际类型参数与方法声明参数类型之间的距离。选择得分最高的方法。 方法选择过程很复杂,使得 Groovy 非常强大。例如,还可以定义元类,这些元类将选择不同的方法,或动态更改方法的返回类型。 与 Java 相比,这种行为是 Groovy 调用时间性能不佳的很大一部分原因。然而,随着 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 上运行它,但对于用户来说,性能很可能很差。 为了确保这一点,一旦 Groovy 核心中实现了 invokedynamic 支持,将进行一次使用反向移植的 InDy 实验。然而,最可能的情况是,这种代码的性能将远远低于静态语言所能提供的性能。 === 静态 Groovy ==== 基于类型推断的分派 本 GEP 旨在在 Groovy 中试验静态编译模式。根据我们之前解释的内容,您应该已经理解静态类型代码意味着与动态代码不同的行为。 如果您期望静态检查和静态编译的代码与动态 Groovy 的行为完全相同,那么您应该立即停止阅读,或者等待 invoke dynamic 支持以期待改进的性能。 如果您完全理解静态编译代码意味着不同的语义,那么您可以继续阅读本 GEP,并帮助我们选择最佳的实现路径。 实际上,这里我们将解释几种选择。 当前的实现依赖于静态类型检查器,该检查器执行类型推断。这意味着对于前面的示例: ``` void foo(String arg) { println 'String' } void foo(Object o) { println 'Object' } def o = 'o 在运行时的类型是 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 的语义不接近。因此,这不是首选的实验性实现。如果您认为此版本应该优先,请随时发送电子邮件至邮件列表,以便我们讨论。您甚至可以分叉当前实现以提供您自己的版本。 === 测试 静态编译现在是 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() // 动态分派 obj->method() // 静态分派 ``` 尽管这个想法听起来很有趣,特别是当你想在单个方法中混合动态和静态代码时,但我们认为它有很多缺点。 首先,它引入了语法更改,这是我们希望尽可能避免的。其次,这个运算符背后的想法是当您 *知道* 对象的类型时执行直接方法调用。但是,没有类型推断,您有两个问题: * 即使指定了“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() // 动态调用,隐式转换为字符串 out->println(content) // 仅基于声明类型的静态方法分派 } 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') } } ``` == 参考文献和有用链接 * https://web.archive.org/web/20150508040816/http://docs.codehaus.org/display/GroovyJSR/GEP+10+-+Static+compilation[GEP-10: 静态编译](网络存档链接) * http://blackdragsview.blogspot.com/2011/10/flow-sensitive-typing.html[流敏感类型?] * https://web.archive.org/web/20150508040745/http://www.jroller.com/melix/entry/groovy_static_type_checker_status[Groovy 静态类型检查器:状态更新](网络存档) === 邮件列表讨论 * https://marc.info/?l=groovy-dev&m=132095119732500&w=2[groovy-dev: Groovy 静态编译]:+ 一场有趣的讨论,动态爱好者解释了他们为什么不希望静态编译 === JIRA 问题 * https://issues.apache.org/jira/browse/GROOVY-5138[GROOVY-5138: GEP-10 - 静态编译] == 更新历史 3 (2012-06-21):: 从 Codehaus wiki 提取的版本 4 (2018-10-26):: 大量细微调整

GEP-10