我知道在最近的Java版本中字符串连接
String test = one + "two"+ three;
将优化使用StringBuilder
。
但是每次遇到这一行时都会生成一个新的StringBuilder
,还是会生成一个Thread Local StringBuilder,然后用于所有字符串连接?
换句话说,我是否可以通过创建自己的线程本地StringBuilder来重新使用经常调用的方法的性能,或者这样做会不会有显着的增益?
我可以为此编写测试但是我想知道它是否可能是编译器/ JVM特定的或者可以更普遍地回答的问题?
答案 0 :(得分:7)
据我所知,没有编译器生成重用StringBuilder
实例的代码,最值得注意的是javac
,而ECJ不生成重用代码。
重要的是要强调不再进行此类重复使用是合理的。假设从ThreadLocal
变量检索实例的代码比TLAB的普通分配更快是不安全的。即使通过尝试增加本地gc周期的潜在成本来回收该实例,只要我们能够确定其成本的分数,我们就无法得出结论。
因此,尝试重用构建器的代码会更加复杂,浪费内存,因为它会让构建器保持活动状态,而不会知道它是否真的会被重用,而没有明显的性能优势。
特别是当我们考虑上述声明时
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来处理的,当实际发生追加时会调用它。