java - Java 8:Class getName会减慢字符串连接链的速度

最近我遇到了一个关于字符串连接的问题,


@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 (OpenJDK 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,其中具有副作用的表达式会中断new的优化,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();




我期望分支预测器找出在每次基准测试调用时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或编译器的bug?

时间:

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 << !



我不能称它为bug或适当的行为” ,这就是HotSpot中实现动态自适应编译的方式。

...