最近、文字列の連結に関する問題に遭遇しました。このベンチマークはそれを要約します:
_@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 に似た問題のように見えます。この場合、副作用がある式は、新しい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();
_
ここで唯一疑わしいのは、実際には1回だけ発生するネイティブメソッドの呼び出しであり、その結果はクラスのフィールドにキャッシュされます。私のベンチマークでは、それをセットアップメソッドに明示的にキャッシュしました。
私はブランチプレディクターが各ベンチマークの呼び出しで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をまだ使用しており、リリースの更新で修正されない場合は、Class.getName()
ホットスポットから手動で。
追伸最新のJDK(11、13、14-eap)では、この問題は再現されません。
わずかに無関係ですが、Java 9 and JEP 280:Indify String Concatenation 以降、invokedynamic
ではなくStringBuilder
を使用して文字列の連結が行われるようになりました。 。 この記事 は、Java 8とJava 9.の間のバイトコードの違いを示しています。
新しいJavaバージョンでベンチマークを再実行しても問題が表示されない場合、コンパイラーが新しいメカニズムを使用しているため、javac
にバグはほとんどありません。ダイビングを確認するinto Java 8の動作は、新しいバージョンでそのような大幅な変更がある場合に有益です。