Java8:类。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位服务器虚拟机,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();
这里唯一可疑的是对本机方法的调用 实际上只有一次,它的结果被缓存在类的字段中。 在我的基准测试中,我将它显式地缓存在setup方法中
我希望branch predictor在每次基准测试调用时都能弄清楚这一点 这个的实际价值。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的适当行为还是编译器错误
我问这个问题是因为一些项目仍在使用Java8,如果它在任何版本更新上都无法修复,那么对我来说,从热点手动取消对Class.getName()
的调用是合理的
注:关于最新的JDK(11、13、14 eap),本期未转载
# 1 楼答案
稍微不相关,但由于Java 9和JEP 280: Indify String Concatenation,字符串连接现在使用
invokedynamic
而不是StringBuilder
完成This article显示了Java8和Java9之间字节码的差异如果在较新的Java版本上重新运行的基准测试没有显示问题,那么
javac
中很可能没有bug,因为编译器现在使用了新的机制。如果在新版本中有如此大的变化,不确定深入研究Java8行为是否有益# 2 楼答案
HotSpot JVM收集每个字节码的执行统计信息。如果相同的代码在不同的上下文中运行,那么结果概要文件将聚合所有上下文中的统计信息。这种效应称为profile pollution
Class.getName()
显然不仅仅是从基准代码中调用的。在JIT开始编译基准测试之前,它已经知道Class.getName()
中的以下条件已多次满足:至少,有足够的时间来处理这个分支在统计学上是重要的。所以,JIT并没有将这个分支排除在编译之外,因此由于可能的副作用,无法优化字符串concat
这甚至不需要是本机方法调用。仅仅是一个常规的现场作业也被认为是一个副作用
下面是一个剖面污染如何影响进一步优化的示例
这基本上是您的基准测试的修改版本,它模拟了
getName()
配置文件的污染。根据新对象上的初步getName()
调用的数量,字符串连接的进一步性能可能会有很大的不同:More examples of profile pollution »
我不能称之为bug或“适当的行为”。这就是HotSpot中动态自适应编译的实现方式