如 JEP 280: Indify String Concatenation 中所写:
将 javac 生成的静态字符串连接字节码序列更改为使用对 JDK 库函数的调用动态调用。这将启用字符串连接的未来优化,而无需进一步更改 javac 发出的字节码。
这里我想了解一下invokedynamic
调用的用途是什么,字节码串联和invokedynamic
有什么不同?
“旧”方式输出一堆面向 StringBuilder
的操作。考虑这个程序:
public class Example {
public static void main(String[] args)
{
String result = args[0] + "-" + args[1] + "-" + args[2];
System.out.println(result);
}
}
如果我们使用 JDK 8 或更早版本编译它,然后使用 javap -c Example
查看字节码,我们会看到如下内容:
public class Example { public Example(); Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return public static void main(java.lang.String[]); Code: 0: new #2 // class java/lang/StringBuilder 3: dup 4: invokespecial #3 // Method java/lang/StringBuilder."<init>":()V 7: aload_0 8: iconst_0 9: aaload 10: invokevirtual #4 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 13: ldc #5 // String - 15: invokevirtual #4 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 18: aload_0 19: iconst_1 20: aaload 21: invokevirtual #4 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 24: ldc #5 // String - 26: invokevirtual #4 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 29: aload_0 30: iconst_2 31: aaload 32: invokevirtual #4 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 35: invokevirtual #6 // Method java/lang/StringBuilder.toString:()Ljava/lang/String; 38: astore_1 39: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream; 42: aload_1 43: invokevirtual #8 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 46: return }
如您所见,它创建了一个 StringBuilder
并使用了 append
。这是众所周知的相当低效的,因为 StringBuilder
中内置缓冲区的默认容量只有 16 个字符,并且 编译器 无法知道提前分配更多,所以它结束了不得不重新分配。它也是一堆方法调用。 (请注意,JVM有时可以检测并重写这些调用模式以提高它们的效率。)
让我们看看 Java 9 生成了什么:
public class Example { public Example(); Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return public static void main(java.lang.String[]); Code: 0: aload_0 1: iconst_0 2: aaload 3: aload_0 4: iconst_1 5: aaload 6: aload_0 7: iconst_2 8: aaload 9: invokedynamic #2, 0 // InvokeDynamic #0:makeConcatWithConstants:(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String; 14: astore_1 15: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream; 18: aload_1 19: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 22: return }
哦,我的,但那更短。 :-) 它从 StringConcatFactory
对 makeConcatWithConstants
进行了一次调用,这在其 Javadoc 中说明了这一点:
有助于创建字符串连接方法的方法,可用于有效连接已知类型的已知数量的参数,可能在类型适应和参数的部分评估之后。这些方法通常用作 invokedynamic 调用站点的引导方法,以支持 Java 编程语言的字符串连接特性。
在深入了解用于优化字符串连接的 invokedynamic
实现的细节之前,我认为,必须先了解一下 What's invokedynamic and how do I use it? 的背景知识
invokedynamic 指令简化并潜在地改进了 JVM 上动态语言的编译器和运行时系统的实现。它通过允许语言实现者使用包含以下步骤的 invokedynamic 指令定义自定义链接行为来做到这一点。
我可能会尝试带您了解这些为实现字符串连接优化而带来的变化。
定义引导方法:- 对于 Java9,invokedynamic 调用站点的引导方法主要支持字符串连接 makeConcat 和 makeConcatWithConstants 是随 StringConcatFactory 实现引入的。 invokedynamic 的使用提供了在运行时之前选择翻译策略的替代方法。 StringConcatFactory 中使用的翻译策略与之前的 java 版本中介绍的 LambdaMetafactory 类似。此外,问题中提到的 JEP 的目标之一是进一步扩展这些策略。
指定常量池条目:- 这些是调用动态指令的附加静态参数,除了 (1) MethodHandles.Lookup 对象,它是在调用动态指令的上下文中创建方法句柄的工厂,(2) 字符串对象,方法动态调用站点中提到的名称和 (3) MethodType 对象,动态调用站点的解析类型签名。代码的链接过程中已经有链接。在运行时,引导方法运行并链接到执行连接的实际代码中。它用适当的invokestatic 调用重写invokedynamic 调用。这会从常量池中加载常量字符串,利用引导方法静态参数将这些和其他常量直接传递给引导方法调用。
使用invokedynamic 指令:- 通过提供在初始调用期间引导调用目标一次的方法,这为惰性链接提供了便利。这里优化的具体想法是用对 java.lang.invoke.StringConcatFactory 的简单调用动态调用替换整个 StringBuilder.append 舞蹈,这将接受需要连接的值。
Indify String Concatenation 提案通过示例说明了使用 Java9 对应用程序进行基准测试,其中编译了与 @T.J. Crowder 共享的类似方法,并且字节码的差异在不同的实现之间相当明显。
我将在这里稍微添加一些细节。要获得的主要部分是字符串连接的完成方式是运行时决定,而不是编译时决定。因此它可以更改,这意味着您已经针对 java-9 编译了一次代码,并且它可以随意更改底层实现,而无需重新编译。
第二点是目前有6 possible strategies for concatenation of String
:
private enum Strategy {
/**
* Bytecode generator, calling into {@link java.lang.StringBuilder}.
*/
BC_SB,
/**
* Bytecode generator, calling into {@link java.lang.StringBuilder};
* but trying to estimate the required storage.
*/
BC_SB_SIZED,
/**
* Bytecode generator, calling into {@link java.lang.StringBuilder};
* but computing the required storage exactly.
*/
BC_SB_SIZED_EXACT,
/**
* MethodHandle-based generator, that in the end calls into {@link java.lang.StringBuilder}.
* This strategy also tries to estimate the required storage.
*/
MH_SB_SIZED,
/**
* MethodHandle-based generator, that in the end calls into {@link java.lang.StringBuilder}.
* This strategy also estimate the required storage exactly.
*/
MH_SB_SIZED_EXACT,
/**
* MethodHandle-based generator, that constructs its own byte[] array from
* the arguments. It computes the required storage exactly.
*/
MH_INLINE_SIZED_EXACT
}
您可以通过参数选择其中任何一个:-Djava.lang.invoke.stringConcat
。请注意,StringBuilder
仍然是一个选项。
不定期副业成功案例分享
+=
。我告诉他们这要看情况而定,但我们不要忘记他们可能会在以后的某个时候找到更好的方法来串接 concat。关键行实际上是倒数第二行:So by being smart, you have caused a performance hit when Java got smarter than you.
invokedynamic
允许在运行时选择不同的连接策略并在第一次调用时绑定,而无需在每次调用时产生方法调用和调度表的开销; nicolai's 文章 here 和 the JEP 中的更多内容。Object
,但是您必须将所有原语装箱...(顺便说一句,Nicolai 在他的优秀文章中介绍了这一点。)String.concat(String)
方法,其实现是就地创建结果字符串的数组。当我们必须对任意对象调用toString()
时,优势就变得没有意义了。同样,当调用接受数组的方法时,调用者必须创建并填充数组,这会降低整体收益。但是现在,这无关紧要,因为新的解决方案基本上就是您正在考虑的,除了它没有装箱开销,不需要创建数组,并且后端可能会为特定场景生成优化的处理程序。