Как экземпляр реализуется в современных реализациях JVM?

Из-за бенчмаркинга, выполненного в других потоках (см. qaru.site/info/17746/...), было показано, что instanceof в Java 6 на самом деле довольно быстро. Как это достигается?

Я знаю, что для одиночного наследования самая быстрая идея имеет некоторую вложенную кодировку интервала, где каждый класс поддерживает [низкий, высокий] интервал, а instanceof - это просто тест включения интервалов, т.е. 2 целых сравнения. Но как это делается для интерфейсов (поскольку интервальное включение работает только для одиночного наследования)? И как обрабатывается загрузка классов? Загрузка новых подклассов означает, что необходимо отрегулировать множество интервалов.

Ответ 1

AFAIK каждый класс знает все классы, которые он расширяет, и интерфейсы, которые он реализует. Они могут храниться в хэш-наборе, предоставляющем время поиска O (1).

Когда код часто принимает одну и ту же ветвь, стоимость может быть почти устранена, так как ЦП может выполнить код в ветке до того, как он определит, нужно ли это сделать, чтобы ветка делала стоимость почти без изменений.

Поскольку микро-бенчмарк был выполнен 4 года назад, я ожидаю, что последние процессоры и JVM будут намного быстрее.

public static void main(String... args) {
    Object[] doubles = new Object[100000];
    Arrays.fill(doubles, 0.0);
    doubles[100] = null;
    doubles[1000] = null;
    for (int i = 0; i < 6; i++) {
        testSameClass(doubles);
        testSuperClass(doubles);
        testInterface(doubles);
    }
}

private static int testSameClass(Object[] doubles) {
    long start = System.nanoTime();
    int count = 0;
    for (Object d : doubles) {
        if (d instanceof Double)
            count++;
    }
    long time = System.nanoTime() - start;
    System.out.printf("instanceof Double took an average of %.1f ns%n", 1.0 * time / doubles.length);
    return count;
}

private static int testSuperClass(Object[] doubles) {
    long start = System.nanoTime();
    int count = 0;
    for (Object d : doubles) {
        if (d instanceof Number)
            count++;
    }
    long time = System.nanoTime() - start;
    System.out.printf("instanceof Number took an average of %.1f ns%n", 1.0 * time / doubles.length);
    return count;
}

private static int testInterface(Object[] doubles) {
    long start = System.nanoTime();
    int count = 0;
    for (Object d : doubles) {
        if (d instanceof Serializable)
            count++;
    }
    long time = System.nanoTime() - start;
    System.out.printf("instanceof Serializable took an average of %.1f ns%n", 1.0 * time / doubles.length);
    return count;
}

окончательно печатает

instanceof Double took an average of 1.3 ns
instanceof Number took an average of 1.3 ns
instanceof Serializable took an average of 1.3 ns

если я изменил "парные" с помощью

    for(int i=0;i<doubles.length;i+=2)
        doubles[i] = "";

Я получаю

instanceof Double took an average of 1.3 ns
instanceof Number took an average of 1.6 ns
instanceof Serializable took an average of 2.2 ns

Примечание. Если я изменяю

if (d instanceof Double)

to

if (d != null && d.getClass() == Double.class)

производительность была одинаковой.

Ответ 2

Я не знаю, как это обрабатывается, но вы можете узнать, посмотрев исходный код JIT-компилятора или сбросив скомпилированный собственный код JIT для некоторых примеров.

А как загружается загрузка класса? Загрузка новых подклассов означает, что необходимо отрегулировать множество интервалов.

Существует несколько ситуаций, когда компилятор JIT оптимизируется на основе предположения, что текущий набор загруженных классов - это все, что есть. Если загружаются новые классы, я понимаю, что компилятор помечает затронутые JIT-компилируемые классы, поскольку их необходимо перекомпилировать.