web-dev-qa-db-ja.com

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 に似た問題のように見えます。この場合、副作用がある式は、新しい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)では、この問題は再現されません。

13
Sergey Tsypanov

わずかに無関係ですが、Java 9 and JEP 280:Indify String Concatenation 以降、invokedynamicではなくStringBuilderを使用して文字列の連結が行われるようになりました。 。 この記事 は、Java 8とJava 9.の間のバイトコードの違いを示しています。

新しいJavaバージョンでベンチマークを再実行しても問題が表示されない場合、コンパイラーが新しいメカニズムを使用しているため、javacにバグはほとんどありません。ダイビングを確認するinto Java 8の動作は、新しいバージョンでそのような大幅な変更がある場合に有益です。

1
Karol Dowbecki