Java用+优化了多少字符串连接?

How much does Java optimize string concatenation with +?

我知道在最近的 Java 版本中字符串连接

String test = one + "two"+ three;

将得到优化以使用 StringBuilder

然而,每次遇到此行时是否会生成一个新的 StringBuilder,或者是否会生成一个单独的 Thread Local StringBuilder 然后用于所有字符串连接?

换句话说,我是否可以通过创建自己的线程本地 StringBuilder 来重用来提高经常调用的方法的性能,或者这样做不会有显着的收益吗?

我可以为此编写一个测试,但我想知道它可能是 compiler/JVM 具体的还是可以更普遍地回答的问题?

据我所知,没有编译器生成重用 StringBuilder 实例的代码,最值得注意的是 javac 和 ECJ 不生成重用代码。

需要强调的是,不这样做是合理的re-use。假设从 ThreadLocal 变量检索实例的代码比从 TLAB 进行普通分配更快是不安全的。即使尝试添加用于回收该实例的本地 gc 循环的潜在成本,就我们可以确定其成本的比例而言,我们也无法得出结论。

因此尝试重用构建器的代码会更加复杂,浪费内存,因为它使构建器保持活动状态而不知道它是否会真正被重用,没有明显的性能优势。

特别是当我们在上述陈述之外考虑到这一点时

  • 像 HotSpot 这样的 JVM 有逃逸分析,它可以完全消除像这样的纯本地分配,也可以消除数组调整大小操作的复制成本
  • 如此复杂的 JVM 通常还专门针对基于 StringBuilder 的串联进行优化,当编译代码遵循通用模式时效果最佳

有了 Java 9,画面又要变了。然后,字符串连接将被编译为 invokedynamic 指令,该指令将在运行时链接到 JRE 提供的工厂(参见 StringConcatFactory)。然后,JRE 将决定代码的外观,这允许根据特定的 JVM 对其进行定制,包括缓冲区 re-use,如果它对特定的 JVM 有好处的话。这也将减少代码大小,因为它只需要一条指令而不是分配序列和多次调用 StringBuilder.

您会惊讶于 jdk-9 字符串连接付出了多少努力。首先 javac 发出 invokedynamic 而不是对 StringBuilder#append 的调用。 invokedynamic 将 return 一个 CallSite 包含一个 MethodHandle(实际上是一系列 MethodHandle)。

因此,对字符串连接实际执行的操作的决定已移至运行时。缺点是第一次连接字符串会变慢(对于相同类型的参数)。

然后在拼接String时有一系列的策略可以选择(可以通过java.lang.invoke.stringConcat参数覆盖默认策略):

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
}

默认策略是:MH_INLINE_SIZED_EXACT这是一个beast!

它使用 package-private 构造函数来构建字符串(这是最快的):

/*
 * Package private constructor which shares value array for speed.
 */
String(byte[] value, byte coder) {
    this.value = value;
    this.coder = coder;
}

首先这个策略创建了所谓的过滤器;这些基本上是将传入参数转换为字符串值的方法句柄。正如人们可能预料的那样,这些 MethodHandle 存储在一个名为 Stringifiers 的 class 中,在大多数情况下,它会生成一个 MethodHandle 调用:

String.valueOf(YourInstance)

因此,如果您有 3 个要连接的对象,将有 3 个方法句柄委托给 String.valueOf(YourObject),这实际上意味着您已将对象转换为字符串。 这个 class 里面有一些我仍然无法理解的调整;比如需要有单独的 classes StringifierMost(仅转换为字符串引用、浮点数和双精度数)和 StringifierAny

因为 MH_INLINE_SIZED_EXACT 表示字节数组计算为精确大小;有一种计算方法。

完成此操作的方法是通过 StringConcatHelper#mixLen 中的方法,这些方法采用输入参数的字符串化版本 (References/float/double)。此时我们知道最终字符串的大小。好吧,我们实际上并不知道它,我们有一个 MethodHandle 可以计算它。

String jdk-9 中还有一个值得一提的变化 - 添加了一个 coder 字段。这是计算字符串的 size/equality/charAt 所必需的。由于大小需要它,因此我们还需要计算它;这是通过 StringConcatHelper#mixCoder.

完成的

此时委托一个将创建您的数组的 MethodHandle 是安全的:

    @ForceInline
    private static byte[] newArray(int length, byte coder) {
        return (byte[]) UNSAFE.allocateUninitializedArray(byte.class, length << coder);
    }

每个元素是如何追加的?通过 StringConcatHelper#prepend.

中的方法

直到现在我们才需要调用需要一个字节的 String 构造函数所需的所有详细信息。


所有这些操作(以及我为简单起见而跳过的许多其他操作)都是通过发出一个 MethodHandle 来处理的,该 MethodHandle 将在实际发生追加时调用。