web-dev-qa-db-ja.com

jdk-9 / jdk-8およびjmhのnewInstance vs new

ここでは、newInstanceまたはnew operator

ソースコードを見ると、newInstanceはるかに遅いになっているように見えます。つまり、非常に多くのセキュリティチェックを行い、リフレクションを使用しています。そして、最初にjdk-8を実行して測定することにしました。以下は、jmhを使用したコードです。

@BenchmarkMode(value = { Mode.AverageTime, Mode.SingleShotTime })
@Warmup(iterations = 5, time = 2, timeUnit = TimeUnit.SECONDS)   
@Measurement(iterations = 5, time = 2, timeUnit = TimeUnit.SECONDS)    
@State(Scope.Benchmark) 
public class TestNewObject {
    public static void main(String[] args) throws RunnerException {

        Options opt = new OptionsBuilder().include(TestNewObject.class.getSimpleName()).build();
        new Runner(opt).run();
    }

    @Fork(1)
    @Benchmark
    public Something newOperator() {
       return new Something();
    }

    @SuppressWarnings("deprecation")
    @Fork(1)
    @Benchmark
    public Something newInstance() throws InstantiationException, IllegalAccessException {
         return Something.class.newInstance();
    }

    static class Something {

    } 
}

私はここで大きな驚きはないと思います(JITはthat bigではなく、この違いを生む多くの最適化を行います):

Benchmark                  Mode  Cnt      Score      Error  Units
TestNewObject.newInstance  avgt    5      7.762 ±    0.745  ns/op
TestNewObject.newOperator  avgt    5      4.714 ±    1.480  ns/op
TestNewObject.newInstance    ss    5  10666.200 ± 4261.855  ns/op
TestNewObject.newOperator    ss    5   1522.800 ± 2558.524  ns/op

ホットコードの違いは2x前後で、シングルショット時間ではさらに悪化します。

ここで、jdk-9(重要な場合は157)に切り替えて、同じコードを実行します。そして結果:

 Benchmark                  Mode  Cnt      Score      Error  Units
 TestNewObject.newInstance  avgt    5    314.307 ±   55.054  ns/op
 TestNewObject.newOperator  avgt    5      4.602 ±    1.084  ns/op
 TestNewObject.newInstance    ss    5  10798.400 ± 5090.458  ns/op
 TestNewObject.newOperator    ss    5   3269.800 ± 4545.827  ns/op

これは、ホットコードのwhooping 50xの違いです。私は最新のjmhバージョン(1.19.SNAPSHOT)を使用しています。

テストにもう1つのメソッドを追加した後:

@Fork(1)
@Benchmark
public Something newInstanceJDK9() throws Exception {
    return Something.class.getDeclaredConstructor().newInstance();
}

Jdk-9の全体的な結果は次のとおりです。

TestNewObject.newInstance      avgt    5    308.342 ±   107.563  ns/op
TestNewObject.newInstanceJDK9  avgt    5     50.659 ±     7.964  ns/op
TestNewObject.newOperator      avgt    5      4.554 ±     0.616  ns/op    

誰かがいくつかの光を当てることができますなぜそんなに大きな違いがあるのか​​

39
Eugene

まず、問題はモジュールシステムとは直接関係ありません。

JDK 9を使用しても、newInstanceの最初のウォームアップイテレーションはJDK 8と同じくらい高速でした。

# Fork: 1 of 1
# Warmup Iteration   1: 10,578 ns/op    <-- Fast!
# Warmup Iteration   2: 246,426 ns/op
# Warmup Iteration   3: 242,347 ns/op

これは、JITコンパイルで何かが壊れていることを意味します。
-XX:+PrintCompilationは、最初の反復後にベンチマークが再コンパイルされたことを確認しました。

10,762 ns/op
# Warmup Iteration   2:    1541  689   !   3       Java.lang.Class::newInstance (160 bytes)   made not entrant
   1548  692 %     4       bench.generated.NewInstance_newInstance_jmhTest::newInstance_avgt_jmhStub @ 13 (56 bytes)
   1552  693       4       bench.generated.NewInstance_newInstance_jmhTest::newInstance_avgt_jmhStub (56 bytes)
   1555  662       3       bench.generated.NewInstance_newInstance_jmhTest::newInstance_avgt_jmhStub (56 bytes)   made not entrant
248,023 ns/op

次に、-XX:+UnlockDiagnosticVMOptions -XX:+PrintInliningはインライン化の問題を指摘しました。

1577  667 %     4       bench.generated.NewInstance_newInstance_jmhTest::newInstance_avgt_jmhStub @ 13 (56 bytes)
                           @ 17   bench.NewInstance::newInstance (6 bytes)   inline (hot)
            !                @ 2   Java.lang.Class::newInstance (160 bytes)   already compiled into a big method

"ビッグメソッドにコンパイル済み"メッセージは、呼び出し先のコンパイル済みサイズがInlineSmallCode値(デフォルトでは2000)よりも大きいため、コンパイラがClass.newInstance呼び出しのインライン化に失敗したことを意味します。

-XX:InlineSmallCode=2500でベンチマークを再実行すると、再び高速になりました。

Benchmark                Mode  Cnt  Score   Error  Units
NewInstance.newInstance  avgt    5  8,847 ± 0,080  ns/op
NewInstance.operatorNew  avgt    5  5,042 ± 0,177  ns/op

JDK 9にはG1がデフォルトGCが追加されました。 Parallel GCにフォールバックすると、デフォルトのInlineSmallCodeでもベンチマークは高速になります。

-XX:+UseParallelGCを使用してJDK 9ベンチマークを再実行します。

Benchmark                Mode  Cnt  Score   Error  Units
NewInstance.newInstance  avgt    5  8,728 ± 0,143  ns/op
NewInstance.operatorNew  avgt    5  4,822 ± 0,096  ns/op

G1では、オブジェクトストアが発生するたびにいくつかのバリアを配置する必要があります。そのため、コンパイルされたコードが少し大きくなり、Class.newInstanceがデフォルトのInlineSmallCode制限を超えます。コンパイルされたClass.newInstanceが大きくなったもう1つの理由は、リフレクションコードがJDK 9でわずかに書き直されたことです。

TL; DRInlineSmallCode制限を超えたため、JITはClass.newInstanceのインライン化に失敗しました。 Class.newInstanceのコンパイル済みバージョンは、JDK 9でのリフレクションコードの変更と、デフォルトGCがG1に変更されたために大きくなりました。

57
apangin

Class.newInstance()の実装は、次の部分を除いてほとんど同じです。

_Constructor<T> tmpConstructor = cachedConstructor;
// Security check (same as in Java.lang.reflect.Constructor)
int modifiers = tmpConstructor.getModifiers();
if (!Reflection.quickCheckMemberAccess(this, modifiers)) {
    Class<?> caller = Reflection.getCallerClass();
    if (newInstanceCallerCache != caller) {
        Reflection.ensureMemberAccess(caller, this, null, modifiers);
        newInstanceCallerCache = caller;
    }
}
_
_Constructor<T> tmpConstructor = cachedConstructor;
// Security check (same as in Java.lang.reflect.Constructor)
Class<?> caller = Reflection.getCallerClass();
if (newInstanceCallerCache != caller) {
    int modifiers = tmpConstructor.getModifiers();
    Reflection.ensureMemberAccess(caller, this, null, modifiers);
    newInstanceCallerCache = caller;
}
_

ご覧のとおり、Java 8にはReflection.getCallerClass()のような高価な操作をバイパスできるquickCheckMemberAccessがありました。新しいモジュールアクセスルールと互換性がないため、このクイックチェックは削除されました。

しかし、それだけではありません。 JVMは、予測可能な型でリフレクションのインスタンス化を最適化し、Something.class.newInstance()は完全に予測可能な型を参照します。この最適化はあまり効果的ではないかもしれません。考えられる理由はいくつかあります。

  • 新しいモジュールアクセスルールはプロセスを複雑にします
  • Class.newInstance()が非推奨になったため、いくつかのサポートが意図的に削除されました(私にはありそうもないようです)
  • 上記の実装コードの変更により、HotSpotは最適化をトリガーする特定のコードパターンを認識できません
4
Holger