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 将在实际发生追加时调用。
我知道在最近的 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 将在实际发生追加时调用。