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来处理的,当实际发生追加时将调用它。