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

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

String test = one + "two"+ three; 

将优化使用StringBuilder

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

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

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

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

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

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

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

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

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

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

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

然后,在连接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这是一个野兽!

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

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

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

 String.valueOf(YourInstance) 

因此,如果您要连接3个对象,则会有3个将委托给String.valueOf(YourObject) ,这实际上意味着您已将对象转换为字符串。 这个课程中有一些我仍然无法理解的调整; 喜欢需要单独的类StringifierMost (它转换为String只引用,浮点数和双精度数)和StringifierAny

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

这样做的方法是通过StringConcatHelper#mixLen方法,它采用输入参数的StringConcatHelper#mixLen版本(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来处理的,当实际发生追加时将调用它。