NewInstance vs new в jdk-9/jdk-8 и jmh
Здесь я видел много потоков, которые сравнивают и пытаются ответить быстрее: 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 делает много оптимизаций, которые делают эту разницу не такой большой):
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).
После добавления еще одного метода к тесту:
@Fork(1)
@Benchmark
public Something newInstanceJDK9() throws Exception {
return Something.class.getDeclaredConstructor().newInstance();
}
Ниже приведены общие результаты n 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
Может кто-то пролить свет на то, почему существует такая большая разница?
Ответы
Ответ 1
Прежде всего, проблема не имеет ничего общего с системой модулей (напрямую).
Я заметил, что даже с 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
", уже скомпилированный в большой метод сообщения означает, что компилятор не смог выполнить встроенный вызов Class.newInstance
, потому что скомпилированный размер вызываемого абонента больше, чем значение InlineSmallCode
(которое равно 2000 по умолчанию).
Когда я повторил тест с -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
.
Rerun JDK 9 с -XX:+UseParallelGC
:
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
стал больше, заключается в том, что код отражения был слегка переписан в JDK 9.
TL; DR JIT не удалось установить Class.newInstance
, потому что предел InlineSmallCode
превышен. Скомпилированная версия Class.newInstance
стала больше из-за изменений кода отражения в JDK 9 и потому, что GC по умолчанию был изменен на G1.
Ответ 2
Реализация Class.newInstance()
в основном идентична, за исключением следующей части:
Java 8:
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;
}
}
Java 9
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 имеет quickCheckMemberAccess
, который позволяет обойти дорогостоящие операции, например Reflection.getCallerClass()
. Эта быстрая проверка была удалена, предположим Id, потому что она не совместима с новыми правилами доступа к модулю.
Но это больше. JVM может оптимизировать рефлексивные экземпляры с предсказуемым типом, а Something.class.newInstance()
относится к совершенно предсказуемому типу. Эта оптимизация могла бы стать менее эффективной. Существует несколько возможных причин:
- новые правила доступа к модулю усложняют процесс
- поскольку
Class.newInstance()
устарел, некоторая поддержка была умышленно удалена (кажется мне маловероятной)
- из-за измененного кода реализации, показанного выше, HotSpot не может распознать определенные шаблоны кода, которые запускают оптимизацию.