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

时间:2016-10-26 16:29:47

标签: java string optimization stringbuilder string-concatenation

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

String test = one + "two"+ three;

将优化使用StringBuilder

但是每次遇到这一行时都会生成一个新的StringBuilder,还是会生成一个Thread Local StringBuilder,然后用于所有字符串连接?

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

我可以为此编写测试但是我想知道它是否可能是编译器/ JVM特定的或者可以更普遍地回答的问题?

2 个答案:

答案 0 :(得分:7)

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

重要的是要强调不再进行此类重复使用是合理的。假设从ThreadLocal变量检索实例的代码比TLAB的普通分配更快是不安全的。即使通过尝试增加本地gc周期的潜在成本来回收该实例,只要我们能够确定其成本的分数,我们就无法得出结论。

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

特别是当我们考虑上述声明时

  • 像HotSpot这样的JVM有Escape Analysis,它可以完全忽略纯粹的本地分配,也可能忽略数组调整大小操作的复制成本
  • 这些复杂的JVM通常还具有专门用于基于StringBuilder的连接的优化,当编译的代码遵循通用模式时,它最有效。

使用Java 9,图片将再次发生变化。然后,字符串连接将被编译为invokedynamic指令,该指令将在运行时链接到JRE提供的工厂(请参阅StringConcatFactory)。然后,JRE将决定代码的外观,它允许将其定制到特定的JVM,包括缓冲区重用,如果它对特定JVM有好处的话。这也将减少代码大小,因为它只需要一条指令而不是分配序列和多次调用StringBuilder

答案 1 :(得分:7)

你会惊讶于jdk-9字符串连接中投入了多少精力。第一个javac发出invokedynamic而不是StringBuilder#append的调用。那个invokedynamic将返回一个CallSite,其中包含一个MethodHandle(实际上是一系列MethodHandles)。

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

然后在连接字符串时可以选择一系列策略(您可以通过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这是野兽!

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

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

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

String.valueOf(YourInstance)

因此,如果您要连接3个对象,则会有3个将委托给String.valueOf(YourObject)的MethodHandles,这实际上意味着您已将对象转换为字符串。 这个课程中有一些我仍然无法理解的调整;比如需要有单独的类StringifierMost(转换为String only References,float和double)和StringifierAny

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

这样做的方法是通过StringConcatHelper#mixLen中的方法,它采用输入参数的Stringified版本(References / float / double)。此时我们知道最终String的大小。好吧,我们实际上并不知道,我们有一个MethodHandle来计算它。

String jdk-9还有一个值得一提的变化 - 添加coder字段。这是计算String的大小/相等/ charAt所必需的。由于尺寸需要,我们还需要计算它;这是通过StringConcatHelper#mixCoder完成的。

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

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

如何追加每个元素?通过StringConcatHelper#prepend中的方法。

现在我们只需要调用带字节的String构造函数所需的所有细节。

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