跳到主要内容

JEP 280:指示字符串连接

概括

更改生成的静态String连接字节码序列以javac使用invokedynamic对 JDK 库函数的调用。这将使未来能够优化String串联,而无需进一步更改 发出的字节码javac

目标

为构建优化的串联处理程序奠定基础String,无需更改 Java 到字节码编译器即可实现。应该尝试多种翻译策略作为这项工作的动机示例。生成(并可能切换到)优化的翻译策略是此 JEP 的一个延伸目标。

非目标

它的目标不是:引入任何可能有助于构建更好的翻译策略的新的String和/或API、重新审视 JIT 编译器对优化串联的支持、支持高级插值用例或探索对 Java 编程语言的其他更改。StringBuilder``String``String

成功指标

  • 稳定的、面向未来的字节码序列由javac.目的是在任何主要 Java 版本中最多更改一次字节码形状。

  • String串联性能不会下降。

  • 启动时间和性能时间不会回归到超出合理水平。

动机

目前javacString串联转换为StringBuilder::append链。有时这种翻译并不是最佳的,有时我们需要StringBuilder适当地调整大小。该-XX:+OptimizeStringConcat选项可以在 JIT 编译器中进行积极的优化,该编译器可以识别StringBuilder附加链并执行调整大小和就地复制。这些优化虽然富有成效,但很脆弱且难以扩展和维护;例如,参见JDK-8043677JDK-8076758JDK-8136469。改变翻译本身是很诱人的javac。例如,请参阅编译器开发列表上的最新提案

当我们考虑更改 中的任何内容时javac,每次我们想要提高性能时都更改编译器和字节码形状似乎很不方便。用户通常期望相同的字节码在较新的 JVM 上运行得更快。要求用户重新编译 Java 程序以提高性能并不友好,而且还会破坏测试矩阵,因为 JVM 应该识别生成的字节码的所有变体。因此,我们可能需要采用一些技巧来声明在字节码中连接字符串的意图,然后在运行时扩展该意图。

换句话说,我们正在引入_类似于_新字节码的东西,“字符串连接”。与 Lambda 工作一样,这缩小了允许语言功能(在本例中为字符串连接)的 JLS 与没有适当设施的 JVM 之间的差距,迫使其将javac其转换为低级代码,并进一步迫使 VM识别所有低级翻译形式的实现。invokedynamic通过在核心库级别提供有保证的字符串连接接口,我们可以一举克服这种阻抗不匹配的问题。接口的实际实现甚至可以使用(可能不安全的)私有 JVM API 进行连接,例如与特定用例相关的固定长度字符串构建器。直接使用这些 APIjavac意味着将它们暴露给公众。

描述

我们将利用以下功能invokedynamic:它通过提供在初始调用期间引导调用目标一次的方法,为惰性链接提供了便利。这种方法并不是什么新鲜事,我们大量借鉴了当前翻译 lambda 表达式的代码。

这个想法是用一个简单的调用来替换整个StringBuilder附加舞蹈,它将接受需要连接的值。例如,invokedynamic``java.lang.invoke.StringConcatFactory

String m(String a, int b) {
return a + "(" + b + ")";
}

目前编译为:

java.lang.String m(java.lang.String, int);
0: new #2 // class java/lang/StringBuilder
3: dup
4: invokespecial #3 // Method java/lang/StringBuilder."<init>":()V
7: aload_1
8: invokevirtual #4 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
11: ldc #5 // String (
13: invokevirtual #4 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
16: iload_2
17: invokevirtual #6 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
20: ldc #7 // String )
22: invokevirtual #4 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
25: invokevirtual #8 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
28: areturn

但即使使用朴素的 indy 翻译(可在建议的实现中使用)-XDstringConcat=indy,它也可以变得更加简单:

java.lang.String m(java.lang.String, int);
0: aload_1
1: ldc #2 // String (
3: iload_2
4: ldc #3 // String )
6: invokedynamic #4, 0 // InvokeDynamic #0:makeConcat:(Ljava/lang/String;Ljava/lang/String;ILjava/lang/String;)Ljava/lang/String;
11: areturn

BootstrapMethods:
0: #19 invokestatic java/lang/invoke/StringConcatFactory.makeConcat:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/invoke/CallSite;

请注意我们如何int在不装箱的情况下传递参数。在运行时,引导方法 (BSM) 运行并链接到执行串联的实际代码中。它invokedynamic用适当的调用重写该调用invokestatic。这会从常量池加载常量字符串,但我们可以利用 BSM 静态参数将这些常量和其他常量直接传递给 BSM 调用。这就是建议的-XDstringConcat=indyWithConstants风味的作用:

java.lang.String m(java.lang.String, int);
0: aload_1
1: iload_2
2: invokedynamic #2, 0 // InvokeDynamic #0:makeConcat:(Ljava/lang/String;I)Ljava/lang/String;
7: areturn

BootstrapMethods:
0: #15 invokestatic java/lang/invoke/StringConcatFactory.makeConcatWithConstants:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/invoke/CallSite;
Method arguments:
#16 \u0001(\u0001)

请注意我们如何仅将动态参数 ("a""b") 传递给 BSM。静态常量在链接期间已经被处理。 BSM 方法提供了一个配方,说明以什么顺序连接动态和静态参数,以及静态参数是什么。该策略还分别处理空值、原语和空字符串。

哪种字节码风格应该是默认值是一个悬而未决的问题。有关字节码形状、连接风格和性能数据的更多详细信息可以在实验笔记中找到。可以在此处找到建议的引导方法 API 。完整的实现可以在 sandbox 分支中找到:

$ hg clone http://hg.openjdk.java.net/jdk9/sandbox sandbox 
$ cd sandbox/
$ sh ./common/bin/hgforest.sh up -r JDK-8085796-indyConcat
$ sh ./configure
$ make images

人们可以通过使用以下命令来查看基线运行时和修补运行时之间的差异:

$ hg diff -r default:JDK-8085796-indyConcat
$ cd langtools/
$ hg diff -r default:JDK-8085796-indyConcat
$ cd jdk/
$ hg diff -r default:JDK-8085796-indyConcat

基准测试可以在这里找到:http://cr.openjdk.java.net/~shade/8085796/

建议的实现成功构建了 JDK,运行回归测试(包括测试Stringconcat 的新测试),并通过所有平台上的冒烟测试。实际的串联可以使用多种策略。我们提出的实现表明,当我们将生成的字节码序列移动javac到注入相同字节码的 BSM 中时,不会出现吞吐量下降,这验证了该方法。优化策略的性能与基线一样好甚至更好,特别是当默认StringBuilder长度不够和/或虚拟机优化失败时。

更多数据请参阅实验笔记。

备择方案

该提案有三种替代方案。

首先,明显的替代方案:javac通过从建议的实现中移植字节码生成策略之一来更改字节码序列本身。虽然短期内很简单,但它打破了与整个 Java 社区的不言而喻的契约:JVM 足够智能,可以识别和优化现有的字节码。每次我们想要调整String连接转换时,我们不能轻易地强迫用户重新编译他们的 Java 程序。每次字节码形状发生变化时,JIT 编译器都被迫识别一种新的形式,这会极大地扩展测试矩阵,并导致毫无戒心的用户出现奇怪的性能(错误)行为。如果我们出于性能原因要更改字节码形状,那么我们最好在主要版本中以面向未来的方式进行一次更改。使用私有 API 来优化字符串内容也是不可能的:javac生成的代码应该仅使用公共 API。

_第二种选择:_引入 varargStringConcat.concat(Object... args)方法,并使用它来连接参数。主要缺点是对原语进行装箱,然后将所有参数装箱到Object[].虽然高级 JIT 可能会识别这些习惯用法,但我们却将自己置于已经很复杂的优化编译器中新的和未经测试的优化的摆布之下。invokedynamic作用大致相同,但它支持开箱即用的各种目标方法签名。

_第三种选择:_继续像我们现在所做的那样,并扩展-XX:+OptimizeStringConcat到覆盖更多案例。缺点是,在 JIT 编译器中进行优化需要toString使用编译器 IR 详细说明转换和存储管理,这使得扩展变得困难,并且需要很少的专业知识才能正确进行。此外,字节码形状中的微小差异和奇怪之处也会被打破-XX:+OptimizeStringConcat,我们必须格外小心以javac避免这种情况。参见例如JDK-8043677JDK-8076758JDK-8136469

所有这三种选择基本上都让我们受到脆弱的编译器优化的摆布。

测试

大多数 Java 代码都会例行执行字符串连接。除了常规的功能和性能测试之外,我们还计划通过微基准测试来研究此更改的性能模型。可能需要开发针对不同策略的新回归测试。

风险和假设

_其他String相关变更。_此更改有可能与其他String相关更改发生冲突,特别是紧凑字符串 (JEP 254)。缓解计划是生成与当前 JDK 完全相同的字节码序列,然后启用“紧凑字符串”的 JVM 将执行相同的代码路径,忽略任何String-concat 转换更改。事实上,我们的初步实验表明我们的建议与紧凑字符串配合得很好。

_java.base 豁免。_由于此功能使用核心库功能(java.lang.invoke)来实现核心语言功能(String连接),因此我们必须使该java.base模块免于使用指定的String连接。否则,当java.lang.invoke.*机器需要Stringconcat 工作时,就会发生循环,而 concat 又需要java.lang.invoke.*机器。此豁免可能会限制从此功能中观察到的性能改进,因为许多java.base类将无法使用它。我们认为这是一个可以接受的缺点,应该由虚拟机的优化编译器来弥补。

_维护成本。_遗留的String-concat 翻译不会消失,因此我们必须维护-XX:+OptimizeStringConcat虚拟机中新翻译策略的优化。仍然需要为无法使用javac指定的 concat 的情况提供后备,特别是某些系统类。String这不会造成重大问题,因为我们的性能故事涉及-XX:+OptimizeStringConcat大多数策略。

_兼容性:不拼写invokedynamic._一旦他们看到指定的字符串连接,他们应该做什么? Lambda 已经有了经验,其中非 indy 拼写平台必须对 indy 进行脱糖处理,并静态地重新生成与 indy 动态发出的代码完全相同的代码。虽然为 naive concat 生成的代码序列String比 简单LambdaFactory,但它仍然在 VM 代码中产生了更多的复杂性。由于无论如何javac仍然保留发出旧连接序列的代码String,因此上述平台可以使用此后备。

_兼容性:其他编译器。_其他(非javac)编译器是否应该发出相同的字节码?我们是否建议更改所有编译器实现以使用新的翻译策略?由于遗留的Stringconcat 并没有消失,其他编译器可以自由地发出它们今天发出的相同字节码序列。他们可以根据自己认为合适的情况,逐步将一些翻译更改为提议的基于 indy 的翻译方案。

_兼容性:字节码操纵器/编织器。_即使没有源代码更改,工具也需要在不习惯的地方处理新的 indy,仅通过重新编译即可。我们不认为这是主要问题,因为工具预计已经能够处理 Lambda,因此它们需要识别invokedynamic.需要对用于检测StringBuilder::append由 生成的链的工具进行更正javac

_静态足迹。_类文件大小可能是String-concat-heavy 代码中的一个问题。有些机器生成的文件几乎耗尽了 CP 中的所有条目,并进行了大量的字符串连接。对于指示的 concat,方法字节码占用空间似乎要低得多String,并且机器的常量池中存在额外的静态开销java.lang.invoke,加上每个 concat 形状的一点开销。有关更多数据,请参阅上面的注释。

_启动开销。_启动开销可能会阻碍这项工作的进展。 Lambdas 的实现引入了这些类型的翻译策略,但只有使用 Lambdas 才需要付出代价。使用 String concat,一旦invokedynamic编译到字节码中,现有代码中的使用就无法转义+(除非您更改代码来避免+StringBuilder显式调用)。这种风险与机器的初始化相关invokedynamic(参见JDK-8086045),因此在这里接受启动回归意味着在需要invokedynamic初始化的其他更改中不存在启动回归,特别是模块系统(JEP 261)。相反,如果模块系统在此更改之前落地,则启动成本将被摊销。

依赖关系