Подтвердить что ты не робот

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    

Может кто-то пролить свет на то, почему существует такая большая разница?

4b9b3361

Ответ 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 не может распознать определенные шаблоны кода, которые запускают оптимизацию.