Java 8:Class.getName() 减慢字符串连接链
Java 8: Class.getName() slows down String concatenation chain
最近我 运行 遇到了一个关于字符串连接的问题。该基准对其进行了总结:
@OutputTimeUnit(TimeUnit.NANOSECONDS)
public class BrokenConcatenationBenchmark {
@Benchmark
public String slow(Data data) {
final Class<? extends Data> clazz = data.clazz;
return "class " + clazz.getName();
}
@Benchmark
public String fast(Data data) {
final Class<? extends Data> clazz = data.clazz;
final String clazzName = clazz.getName();
return "class " + clazzName;
}
@State(Scope.Thread)
public static class Data {
final Class<? extends Data> clazz = getClass();
@Setup
public void setup() {
//explicitly load name via native method Class.getName0()
clazz.getName();
}
}
}
在 JDK 1.8.0_222(打开JDK 64 位服务器 VM,25.222-b10)我得到以下结果:
Benchmark Mode Cnt Score Error Units
BrokenConcatenationBenchmark.fast avgt 25 22,253 ± 0,962 ns/op
BrokenConcatenationBenchmark.fast:·gc.alloc.rate avgt 25 9824,603 ± 400,088 MB/sec
BrokenConcatenationBenchmark.fast:·gc.alloc.rate.norm avgt 25 240,000 ± 0,001 B/op
BrokenConcatenationBenchmark.fast:·gc.churn.PS_Eden_Space avgt 25 9824,162 ± 397,745 MB/sec
BrokenConcatenationBenchmark.fast:·gc.churn.PS_Eden_Space.norm avgt 25 239,994 ± 0,522 B/op
BrokenConcatenationBenchmark.fast:·gc.churn.PS_Survivor_Space avgt 25 0,040 ± 0,011 MB/sec
BrokenConcatenationBenchmark.fast:·gc.churn.PS_Survivor_Space.norm avgt 25 0,001 ± 0,001 B/op
BrokenConcatenationBenchmark.fast:·gc.count avgt 25 3798,000 counts
BrokenConcatenationBenchmark.fast:·gc.time avgt 25 2241,000 ms
BrokenConcatenationBenchmark.slow avgt 25 54,316 ± 1,340 ns/op
BrokenConcatenationBenchmark.slow:·gc.alloc.rate avgt 25 8435,703 ± 198,587 MB/sec
BrokenConcatenationBenchmark.slow:·gc.alloc.rate.norm avgt 25 504,000 ± 0,001 B/op
BrokenConcatenationBenchmark.slow:·gc.churn.PS_Eden_Space avgt 25 8434,983 ± 198,966 MB/sec
BrokenConcatenationBenchmark.slow:·gc.churn.PS_Eden_Space.norm avgt 25 503,958 ± 1,000 B/op
BrokenConcatenationBenchmark.slow:·gc.churn.PS_Survivor_Space avgt 25 0,127 ± 0,011 MB/sec
BrokenConcatenationBenchmark.slow:·gc.churn.PS_Survivor_Space.norm avgt 25 0,008 ± 0,001 B/op
BrokenConcatenationBenchmark.slow:·gc.count avgt 25 3789,000 counts
BrokenConcatenationBenchmark.slow:·gc.time avgt 25 2245,000 ms
这看起来像一个类似于 JDK-8043677 的问题,其中表达式有副作用
打破了新 StringBuilder.append().append().toString()
链的优化。
但是Class.getName()
的代码本身好像没有什么副作用:
private transient String name;
public String getName() {
String name = this.name;
if (name == null) {
this.name = name = this.getName0();
}
return name;
}
private native String getName0();
这里唯一可疑的是对本地方法的调用
实际上只有一次,其结果缓存在class 的字段中。
在我的基准测试中,我已经在设置方法中明确缓存了它。
我希望分支预测器能够在每次基准调用时计算出这一点
this.name 的实际值永远不会为 null 并优化整个表达式。
然而,对于 BrokenConcatenationBenchmark.fast()
我有这个:
@ 19 tsypanov.strings.benchmark.concatenation.BrokenConcatenationBenchmark::fast (30 bytes) force inline by CompileCommand
@ 6 java.lang.Class::getName (18 bytes) inline (hot)
@ 14 java.lang.Class::initClassName (0 bytes) native method
@ 14 java.lang.StringBuilder::<init> (7 bytes) inline (hot)
@ 19 java.lang.StringBuilder::append (8 bytes) inline (hot)
@ 23 java.lang.StringBuilder::append (8 bytes) inline (hot)
@ 26 java.lang.StringBuilder::toString (35 bytes) inline (hot)
即编译器能够内联所有内容,因为 BrokenConcatenationBenchmark.slow()
它是不同的:
@ 19 tsypanov.strings.benchmark.concatenation.BrokenConcatenationBenchmark::slow (28 bytes) force inline by CompilerOracle
@ 9 java.lang.StringBuilder::<init> (7 bytes) inline (hot)
@ 3 java.lang.AbstractStringBuilder::<init> (12 bytes) inline (hot)
@ 1 java.lang.Object::<init> (1 bytes) inline (hot)
@ 14 java.lang.StringBuilder::append (8 bytes) inline (hot)
@ 2 java.lang.AbstractStringBuilder::append (50 bytes) inline (hot)
@ 10 java.lang.String::length (6 bytes) inline (hot)
@ 21 java.lang.AbstractStringBuilder::ensureCapacityInternal (27 bytes) inline (hot)
@ 17 java.lang.AbstractStringBuilder::newCapacity (39 bytes) inline (hot)
@ 20 java.util.Arrays::copyOf (19 bytes) inline (hot)
@ 11 java.lang.Math::min (11 bytes) (intrinsic)
@ 14 java.lang.System::arraycopy (0 bytes) (intrinsic)
@ 35 java.lang.String::getChars (62 bytes) inline (hot)
@ 58 java.lang.System::arraycopy (0 bytes) (intrinsic)
@ 18 java.lang.Class::getName (21 bytes) inline (hot)
@ 11 java.lang.Class::getName0 (0 bytes) native method
@ 21 java.lang.StringBuilder::append (8 bytes) inline (hot)
@ 2 java.lang.AbstractStringBuilder::append (50 bytes) inline (hot)
@ 10 java.lang.String::length (6 bytes) inline (hot)
@ 21 java.lang.AbstractStringBuilder::ensureCapacityInternal (27 bytes) inline (hot)
@ 17 java.lang.AbstractStringBuilder::newCapacity (39 bytes) inline (hot)
@ 20 java.util.Arrays::copyOf (19 bytes) inline (hot)
@ 11 java.lang.Math::min (11 bytes) (intrinsic)
@ 14 java.lang.System::arraycopy (0 bytes) (intrinsic)
@ 35 java.lang.String::getChars (62 bytes) inline (hot)
@ 58 java.lang.System::arraycopy (0 bytes) (intrinsic)
@ 24 java.lang.StringBuilder::toString (17 bytes) inline (hot)
所以问题是这是 JVM 的适当行为还是编译器错误?
我问这个问题是因为一些项目仍在使用 Java 8,如果它不会在任何版本更新中修复,那么对我来说,提升对 [=18 的调用是合理的=] 手动从热点。
P.S。在最新的 JDKs(11、13、14-eap)上,问题没有重现。
稍微不相关,但由于 Java 9 和 JEP 280: Indify String Concatenation the string concatenation is now done with invokedynamic
and not StringBuilder
. This article 显示了 Java 8 和 Java 9 之间的字节码差异。
如果在较新的 Java 版本上重新 运行 的基准测试没有显示问题,那么 javac
中很可能没有错误,因为编译器现在使用新机制。如果新版本中有如此重大的变化,我不确定深入研究 Java 8 行为是否有益。
HotSpot JVM 收集每个字节码的执行统计信息。如果相同的代码在不同的上下文中是 运行,则结果配置文件将汇总来自所有上下文的统计信息。这种效应被称为 profile pollution。
Class.getName()
显然不仅从您的基准代码中调用。在 JIT 开始编译基准之前,它已经知道 Class.getName()
中的以下条件被多次满足:
if (name == null)
this.name = name = getName0();
至少,有足够的时间来处理这个分支在统计上的重要性。因此,JIT 没有从编译中排除这个分支,因此由于可能的副作用而无法优化字符串连接。
这甚至不需要是本地方法调用。仅常规字段分配也被视为副作用。
这是配置文件污染如何损害进一步优化的示例。
@State(Scope.Benchmark)
public class StringConcat {
private final MyClass clazz = new MyClass();
static class MyClass {
private String name;
public String getName() {
if (name == null) name = "ZZZ";
return name;
}
}
@Param({"1", "100", "400", "1000"})
private int pollutionCalls;
@Setup
public void setup() {
for (int i = 0; i < pollutionCalls; i++) {
new MyClass().getName();
}
}
@Benchmark
public String fast() {
String clazzName = clazz.getName();
return "str " + clazzName;
}
@Benchmark
public String slow() {
return "str " + clazz.getName();
}
}
这基本上是模拟 getName()
配置文件污染的基准测试的修改版本。根据对新对象的初步 getName()
调用次数,字符串连接的进一步性能可能会有很大差异:
Benchmark (pollutionCalls) Mode Cnt Score Error Units
StringConcat.fast 1 avgt 15 11,458 ± 0,076 ns/op
StringConcat.fast 100 avgt 15 11,690 ± 0,222 ns/op
StringConcat.fast 400 avgt 15 12,131 ± 0,105 ns/op
StringConcat.fast 1000 avgt 15 12,194 ± 0,069 ns/op
StringConcat.slow 1 avgt 15 11,771 ± 0,105 ns/op
StringConcat.slow 100 avgt 15 11,963 ± 0,212 ns/op
StringConcat.slow 400 avgt 15 26,104 ± 0,202 ns/op << !
StringConcat.slow 1000 avgt 15 26,108 ± 0,436 ns/op << !
我不能将其称为错误或 "appropriate behaviour"。 HotSpot就是这样实现动态自适应编译的。
最近我 运行 遇到了一个关于字符串连接的问题。该基准对其进行了总结:
@OutputTimeUnit(TimeUnit.NANOSECONDS)
public class BrokenConcatenationBenchmark {
@Benchmark
public String slow(Data data) {
final Class<? extends Data> clazz = data.clazz;
return "class " + clazz.getName();
}
@Benchmark
public String fast(Data data) {
final Class<? extends Data> clazz = data.clazz;
final String clazzName = clazz.getName();
return "class " + clazzName;
}
@State(Scope.Thread)
public static class Data {
final Class<? extends Data> clazz = getClass();
@Setup
public void setup() {
//explicitly load name via native method Class.getName0()
clazz.getName();
}
}
}
在 JDK 1.8.0_222(打开JDK 64 位服务器 VM,25.222-b10)我得到以下结果:
Benchmark Mode Cnt Score Error Units
BrokenConcatenationBenchmark.fast avgt 25 22,253 ± 0,962 ns/op
BrokenConcatenationBenchmark.fast:·gc.alloc.rate avgt 25 9824,603 ± 400,088 MB/sec
BrokenConcatenationBenchmark.fast:·gc.alloc.rate.norm avgt 25 240,000 ± 0,001 B/op
BrokenConcatenationBenchmark.fast:·gc.churn.PS_Eden_Space avgt 25 9824,162 ± 397,745 MB/sec
BrokenConcatenationBenchmark.fast:·gc.churn.PS_Eden_Space.norm avgt 25 239,994 ± 0,522 B/op
BrokenConcatenationBenchmark.fast:·gc.churn.PS_Survivor_Space avgt 25 0,040 ± 0,011 MB/sec
BrokenConcatenationBenchmark.fast:·gc.churn.PS_Survivor_Space.norm avgt 25 0,001 ± 0,001 B/op
BrokenConcatenationBenchmark.fast:·gc.count avgt 25 3798,000 counts
BrokenConcatenationBenchmark.fast:·gc.time avgt 25 2241,000 ms
BrokenConcatenationBenchmark.slow avgt 25 54,316 ± 1,340 ns/op
BrokenConcatenationBenchmark.slow:·gc.alloc.rate avgt 25 8435,703 ± 198,587 MB/sec
BrokenConcatenationBenchmark.slow:·gc.alloc.rate.norm avgt 25 504,000 ± 0,001 B/op
BrokenConcatenationBenchmark.slow:·gc.churn.PS_Eden_Space avgt 25 8434,983 ± 198,966 MB/sec
BrokenConcatenationBenchmark.slow:·gc.churn.PS_Eden_Space.norm avgt 25 503,958 ± 1,000 B/op
BrokenConcatenationBenchmark.slow:·gc.churn.PS_Survivor_Space avgt 25 0,127 ± 0,011 MB/sec
BrokenConcatenationBenchmark.slow:·gc.churn.PS_Survivor_Space.norm avgt 25 0,008 ± 0,001 B/op
BrokenConcatenationBenchmark.slow:·gc.count avgt 25 3789,000 counts
BrokenConcatenationBenchmark.slow:·gc.time avgt 25 2245,000 ms
这看起来像一个类似于 JDK-8043677 的问题,其中表达式有副作用
打破了新 StringBuilder.append().append().toString()
链的优化。
但是Class.getName()
的代码本身好像没有什么副作用:
private transient String name;
public String getName() {
String name = this.name;
if (name == null) {
this.name = name = this.getName0();
}
return name;
}
private native String getName0();
这里唯一可疑的是对本地方法的调用 实际上只有一次,其结果缓存在class 的字段中。 在我的基准测试中,我已经在设置方法中明确缓存了它。
我希望分支预测器能够在每次基准调用时计算出这一点 this.name 的实际值永远不会为 null 并优化整个表达式。
然而,对于 BrokenConcatenationBenchmark.fast()
我有这个:
@ 19 tsypanov.strings.benchmark.concatenation.BrokenConcatenationBenchmark::fast (30 bytes) force inline by CompileCommand
@ 6 java.lang.Class::getName (18 bytes) inline (hot)
@ 14 java.lang.Class::initClassName (0 bytes) native method
@ 14 java.lang.StringBuilder::<init> (7 bytes) inline (hot)
@ 19 java.lang.StringBuilder::append (8 bytes) inline (hot)
@ 23 java.lang.StringBuilder::append (8 bytes) inline (hot)
@ 26 java.lang.StringBuilder::toString (35 bytes) inline (hot)
即编译器能够内联所有内容,因为 BrokenConcatenationBenchmark.slow()
它是不同的:
@ 19 tsypanov.strings.benchmark.concatenation.BrokenConcatenationBenchmark::slow (28 bytes) force inline by CompilerOracle
@ 9 java.lang.StringBuilder::<init> (7 bytes) inline (hot)
@ 3 java.lang.AbstractStringBuilder::<init> (12 bytes) inline (hot)
@ 1 java.lang.Object::<init> (1 bytes) inline (hot)
@ 14 java.lang.StringBuilder::append (8 bytes) inline (hot)
@ 2 java.lang.AbstractStringBuilder::append (50 bytes) inline (hot)
@ 10 java.lang.String::length (6 bytes) inline (hot)
@ 21 java.lang.AbstractStringBuilder::ensureCapacityInternal (27 bytes) inline (hot)
@ 17 java.lang.AbstractStringBuilder::newCapacity (39 bytes) inline (hot)
@ 20 java.util.Arrays::copyOf (19 bytes) inline (hot)
@ 11 java.lang.Math::min (11 bytes) (intrinsic)
@ 14 java.lang.System::arraycopy (0 bytes) (intrinsic)
@ 35 java.lang.String::getChars (62 bytes) inline (hot)
@ 58 java.lang.System::arraycopy (0 bytes) (intrinsic)
@ 18 java.lang.Class::getName (21 bytes) inline (hot)
@ 11 java.lang.Class::getName0 (0 bytes) native method
@ 21 java.lang.StringBuilder::append (8 bytes) inline (hot)
@ 2 java.lang.AbstractStringBuilder::append (50 bytes) inline (hot)
@ 10 java.lang.String::length (6 bytes) inline (hot)
@ 21 java.lang.AbstractStringBuilder::ensureCapacityInternal (27 bytes) inline (hot)
@ 17 java.lang.AbstractStringBuilder::newCapacity (39 bytes) inline (hot)
@ 20 java.util.Arrays::copyOf (19 bytes) inline (hot)
@ 11 java.lang.Math::min (11 bytes) (intrinsic)
@ 14 java.lang.System::arraycopy (0 bytes) (intrinsic)
@ 35 java.lang.String::getChars (62 bytes) inline (hot)
@ 58 java.lang.System::arraycopy (0 bytes) (intrinsic)
@ 24 java.lang.StringBuilder::toString (17 bytes) inline (hot)
所以问题是这是 JVM 的适当行为还是编译器错误?
我问这个问题是因为一些项目仍在使用 Java 8,如果它不会在任何版本更新中修复,那么对我来说,提升对 [=18 的调用是合理的=] 手动从热点。
P.S。在最新的 JDKs(11、13、14-eap)上,问题没有重现。
稍微不相关,但由于 Java 9 和 JEP 280: Indify String Concatenation the string concatenation is now done with invokedynamic
and not StringBuilder
. This article 显示了 Java 8 和 Java 9 之间的字节码差异。
如果在较新的 Java 版本上重新 运行 的基准测试没有显示问题,那么 javac
中很可能没有错误,因为编译器现在使用新机制。如果新版本中有如此重大的变化,我不确定深入研究 Java 8 行为是否有益。
HotSpot JVM 收集每个字节码的执行统计信息。如果相同的代码在不同的上下文中是 运行,则结果配置文件将汇总来自所有上下文的统计信息。这种效应被称为 profile pollution。
Class.getName()
显然不仅从您的基准代码中调用。在 JIT 开始编译基准之前,它已经知道 Class.getName()
中的以下条件被多次满足:
if (name == null)
this.name = name = getName0();
至少,有足够的时间来处理这个分支在统计上的重要性。因此,JIT 没有从编译中排除这个分支,因此由于可能的副作用而无法优化字符串连接。
这甚至不需要是本地方法调用。仅常规字段分配也被视为副作用。
这是配置文件污染如何损害进一步优化的示例。
@State(Scope.Benchmark)
public class StringConcat {
private final MyClass clazz = new MyClass();
static class MyClass {
private String name;
public String getName() {
if (name == null) name = "ZZZ";
return name;
}
}
@Param({"1", "100", "400", "1000"})
private int pollutionCalls;
@Setup
public void setup() {
for (int i = 0; i < pollutionCalls; i++) {
new MyClass().getName();
}
}
@Benchmark
public String fast() {
String clazzName = clazz.getName();
return "str " + clazzName;
}
@Benchmark
public String slow() {
return "str " + clazz.getName();
}
}
这基本上是模拟 getName()
配置文件污染的基准测试的修改版本。根据对新对象的初步 getName()
调用次数,字符串连接的进一步性能可能会有很大差异:
Benchmark (pollutionCalls) Mode Cnt Score Error Units
StringConcat.fast 1 avgt 15 11,458 ± 0,076 ns/op
StringConcat.fast 100 avgt 15 11,690 ± 0,222 ns/op
StringConcat.fast 400 avgt 15 12,131 ± 0,105 ns/op
StringConcat.fast 1000 avgt 15 12,194 ± 0,069 ns/op
StringConcat.slow 1 avgt 15 11,771 ± 0,105 ns/op
StringConcat.slow 100 avgt 15 11,963 ± 0,212 ns/op
StringConcat.slow 400 avgt 15 26,104 ± 0,202 ns/op << !
StringConcat.slow 1000 avgt 15 26,108 ± 0,436 ns/op << !
我不能将其称为错误或 "appropriate behaviour"。 HotSpot就是这样实现动态自适应编译的。