ChatGPT解决这个技术问题 Extra ChatGPT

Java 9 中如何实现字符串连接?

JEP 280: Indify String Concatenation 中所写:

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

这里我想了解一下invokedynamic调用的用途是什么,字节码串联和invokedynamic有什么不同?

wrote about that不久前 - 如果这有帮助,我会将其浓缩为一个答案。
另外,看看这个视频,它很好地解释了新字符串连接机制的要点:youtu.be/wIyeOaitmWM?t=37m58s
@ZhekaKozlov 我希望我可以两次对您的评论进行投票,来自实际实施所有这些的人的链接是最好的。
@Nicolai:那太好了,并且比这里的任何其他人(包括我的)都更好。我的答案的任何部分你想在你这样做时加入,请随意 - 如果你(基本上)将整个事情作为更广泛答案的一部分包括在内,我将删除我的。或者,如果您只想添加到我的答案中,因为它很明显,我已将其设为社区 wiki。

3
3 revs

“旧”方式输出一堆面向 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
}

哦,我的,但那更短。 :-) 它从 StringConcatFactorymakeConcatWithConstants 进行了一次调用,这在其 Javadoc 中说明了这一点:

有助于创建字符串连接方法的方法,可用于有效连接已知类型的已知数量的参数,可能在类型适应和参数的部分评估之后。这些方法通常用作 invokedynamic 调用站点的引导方法,以支持 Java 编程语言的字符串连接特性。


这让我想起了大约 6 年前写的一个答案:stackoverflow.com/a/7586780/330057 - 有人问他们是否应该制作一个 StringBuilder 或只是在他们的 for 循环中使用普通的旧 +=。我告诉他们这要看情况而定,但我们不要忘记他们可能会在以后的某个时候找到更好的方法来串接 concat。关键行实际上是倒数第二行:So by being smart, you have caused a performance hit when Java got smarter than you.
@corsiKa:哈哈!但是哇,到那里花了很长时间(我不是说六年,我是说 22 年左右...... :-))
@supercat:据我了解,有几个原因,尤其是创建一个可变参数数组以传递给性能关键路径上的方法并不理想。此外,使用 invokedynamic 允许在运行时选择不同的连接策略并在第一次调用时绑定,而无需在每次调用时产生方法调用和调度表的开销; nicolai's 文章 herethe JEP 中的更多内容。
@supercat:还有一个事实是它不能很好地与非字符串一起使用,因为它们必须预先转换为字符串而不是转换为最终结果;更无效率。可以使它成为Object,但是您必须将所有原语装箱...(顺便说一句,Nicolai 在他的优秀文章中介绍了这一点。)
@supercat 我指的是已经存在的 String.concat(String) 方法,其实现是就地创建结果字符串的数组。当我们必须对任意对象调用 toString() 时,优势就变得没有意义了。同样,当调用接受数组的方法时,调用者必须创建并填充数组,这会降低整体收益。但是现在,这无关紧要,因为新的解决方案基本上就是您正在考虑的,除了它没有装箱开销,不需要创建数组,并且后端可能会为特定场景生成优化的处理程序。
F
Farzad Karimi

在深入了解用于优化字符串连接的 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 共享的类似方法,并且字节码的差异在不同的实现之间相当明显。


E
Eugene

我将在这里稍微添加一些细节。要获得的主要部分是字符串连接的完成方式是运行时决定,而不是编译时决定。因此它可以更改,这意味着您已经针对 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 仍然是一个选项。