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


111

JEP 280:Indify String Concatenation

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

在这里,我想了解invokedynamic调用的用途是什么,字节码的连接与有何不同invokedynamic


11
不久前,写了关于它的文章-如果有帮助,我将其浓缩成一个答案。
Nicolai

10
另外,请观看此视频,它很好地解释了新的字符串连接机制的要点:youtu.be/wIyeOaitmWM?
t=37m58s

3
@ZhekaKozlov我希望我能对您的评论进行两次投票,实际上来自所有实施人员的链接是最好的。
尤金(Eugene)

2
@Nicolai:太好了,比这里的任何其他答案(包括我的答案)都更好。您想在回答时将其合并的任何部分都可以自由使用-如果您将整个内容(基本上)包含为更广泛的回答的一部分,我将删除我的全部。或者,如果您只是想添加到我的答案中,因为它很明显,那么我将它设为社区Wiki。
TJ Crowder

Answers:


95

“旧”方式输出一堆 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查看字节码,则会看到类似以下内容的内容:

公共类示例{
  公共Example();
    码:
       0:加载_0
       1:invokespecial#1 //方法java / lang / Object。“ <init>” :()V
       4:返回

  公共静态void main(java.lang.String []);
    码:
       0:新#2 //类java / lang / StringBuilder
       3:dup
       4:invokespecial#3 //方法java / lang / StringBuilder。“ <init>” :()V
       7:aload_0
       8:iconst_0
       9:aaload
      10:invokevirtual#4 //方法java / lang / StringBuilder.append:(Ljava / lang / String;)Ljava / lang / StringBuilder;
      13:ldc#5 //字符串-
      15:invokevirtual#4 //方法java / lang / StringBuilder.append:(Ljava / lang / String;)Ljava / lang / StringBuilder;
      18:aload_0
      19:iconst_1
      20:aaload
      21:invokevirtual#4 //方法java / lang / StringBuilder.append:(Ljava / lang / String;)Ljava / lang / StringBuilder;
      24:ldc#5 //字符串-
      26:invokevirtual#4 //方法java / lang / StringBuilder.append:(Ljava / lang / String;)Ljava / lang / StringBuilder;
      29:aload_0
      30:iconst_2
      31:aaload
      32:invokevirtual#4 //方法java / lang / StringBuilder.append:(Ljava / lang / String;)Ljava / lang / StringBuilder;
      35:invokevirtual#6 //方法java / lang / StringBuilder.toString :()Ljava / lang / String;
      38:astore_1
      39:getstatic#7 //字段java / lang / System.out:Ljava / io / PrintStream;
      42:aload_1
      43:invokevirtual#8 //方法java / io / PrintStream.println:(Ljava / lang / String;)V
      46:返回
}

如您所见,它创建了StringBuilder并使用append。由于内置缓冲区的默认容量StringBuilder仅为16个字符,这是众所周知的效率很低的问题,并且编译器无法知道要提前分配更多内容,因此最终不得不重新分配。这也是一堆方法调用。(请注意,JVM 有时可以检测并重写这些调用模式,以使它们更有效。)

让我们看一下Java 9生成的内容:

公共类示例{
  公共Example();
    码:
       0:加载_0
       1:invokespecial#1 //方法java / lang / Object。“ <init>” :()V
       4:返回

  公共静态void main(java.lang.String []);
    码:
       0:加载_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 //字段java / lang / System.out:Ljava / io / PrintStream;
      18:aload_1
      19:invokevirtual#4 //方法java / io / PrintStream.println:(Ljava / lang / String;)V
      22:返回
}

哦,我的,但是短些。:-)对makeConcatWithConstantsfrom 进行一次调用StringConcatFactory,该调用在其Javadoc中这样说:

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


41
这让我想起了大约6年前我今天写的一个答案:stackoverflow.com/a/7586780/330057-有人问他们是应该制作StringBuilder还是+=在其for循环中使用简单的old 。我告诉他们这取决于情况,但请不要忘记,他们可能会在某个时候找到更好的方法来串连。关键线确实是倒数第二行:So by being smart, you have caused a performance hit when Java got smarter than you.
corsiKa '17

3
@corsiKa:大声笑!但是哇,花了很长时间才到达那里(我不是说六年,我是说22岁左右……:
TJ Crowder

1
@supercat:据我了解,有两个原因,其中最重要的一点是,创建一个varargs数组以传递给性能关键路径上的方法并不理想。同样,使用invokedynamic允许在运行时选择不同的串联策略并在第一次调用时绑定,而无需每次调用​​都产生方法调用和调度表的开销。更Nicolai的文章在这里,并在该JEP
TJ Crowder

1
@supercat:还有一个事实,那就是非字符串不能很好地发挥作用,因为它们必须预先转换为字符串,而不是转换为最终结果;效率低下。可以做到Object,但是然后您必须将所有原语装箱……(尼古拉在他的出色文章中提到,顺便说一句)
TJ Crowder

2
@supercat我指的是已经存在的String.concat(String)方法,其实现是就地创建结果字符串的数组。当我们必须toString()对任意对象进行调用时,优点变得毫无意义。同样,当调用接受数组的方法时,调用者必须创建并填充数组,这会降低总体收益。但是现在,这已经无关紧要了,因为新解决方案基本上就是您正在考虑的东西,除了它没有装箱费用,不需要创建数组而且后端可以为特定场景生成优化的处理程序。
Holger

20

invokedynamic在我看来,在深入探讨用于优化字符串连接的实现的细节之前,必须先了解一下什么是invokedynamic,以及如何使用它?

invokedynamic 指令简化并可能改善JVM上用于动态语言的编译器和运行时系统的实现。它通过允许语言实现者使用invokedynamic涉及以下以下步骤的指令来定义自定义链接行为来实现此目的。


我可能会尝试并带您完成为实现String串联优化所带来的更改。

  • 定义Bootstrap方法:-在Java9中,invokedynamic调用站点的bootstrap方法主要支持字符串连接makeConcatmakeConcatWithConstants并随StringConcatFactory实现一起引入。

    使用invokedynamic可以选择一种转换策略,直到运行时。中使用的翻译策略StringConcatFactory类似于LambdaMetafactory先前的Java版本中引入的策略。另外,问题中提到的JEP的目标之一是进一步扩展这些策略。

  • 指定常量池条目:-这些是invokedynamic指令的附加静态参数,而不是(1)MethodHandles.Lookup对象,该对象是在invokedynamic指令上下文中创建方法句柄的工厂,(2)一个String对象,在动态调用中提到的方法名称站点和(3)MethodType对象,即动态调用站点的已解析类型签名。

    在代码链接期间已经存在链接。在运行时,bootstrap方法将运行并链接到进行级联的实际代码中。它将invokedynamic使用适当的invokestatic呼叫重写该呼叫。这将从常量池中加载常量字符串,利用bootstrap方法的静态args将这些常量和其他常量直接传递给bootstrap方法调用。

  • 使用invokedynamic指令:-通过提供在初始调用期间一次引导调用目标的方法,这为延迟链接提供了便利。优化的具体思路是StringBuilder.append,通过简单invokedynamic调用替换整个舞蹈,该调用java.lang.invoke.StringConcatFactory将接受需要连接的值。

所述Indify字符串连接用一个例子提案状态与Java9应用程序,其中一个相似的方法如通过共享的基准@TJ克劳德被编译并在字节码的差的变实现之间相当可见。


17

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.