ここでは、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
誰かがいくつかの光を当てることができますなぜそんなに大きな違いがあるのか?
まず、問題はモジュールシステムとは直接関係ありません。
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; DR
InlineSmallCode
制限を超えたため、JITはClass.newInstance
のインライン化に失敗しました。Class.newInstance
のコンパイル済みバージョンは、JDK 9でのリフレクションコードの変更と、デフォルトGCがG1に変更されたために大きくなりました。
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()
が非推奨になったため、いくつかのサポートが意図的に削除されました(私にはありそうもないようです)