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

Почему hashCode медленнее, чем аналогичный метод?

Как правило, Java оптимизирует виртуальные вызовы на основе количества реализаций, встречающихся на данной стороне вызова. Это легко увидеть в результатах моего benchmark, когда вы смотрите myCode, что является тривиальным методом, возвращающим сохраненный int. Там тривиальный

static abstract class Base {
    abstract int myCode();
}

с несколькими идентичными реализациями, такими как

static class A extends Base {
    @Override int myCode() {
        return n;
    }
    @Override public int hashCode() {
        return n;
    }
    private final int n = nextInt();
}

С увеличением числа реализаций время вызова метода растет с 0,4 нс до 1,2 нс для двух реализаций до 11,6 нс, а затем медленно растет. Когда JVM увидела множественную реализацию, т.е. С preload=true, тайминги немного отличаются (из-за теста instanceof).

До сих пор все ясно, однако, hashCode ведет себя по-другому. Особенно это в 8-10 раз медленнее в трех случаях. Любая идея почему?

UPDATE

Мне было любопытно, можно ли помочь бедным hashCode, отправив вручную, и это может быть много.

timing

Несколько ветвей отлично справились с работой:

if (o instanceof A) {
    result += ((A) o).hashCode();
} else if (o instanceof B) {
    result += ((B) o).hashCode();
} else if (o instanceof C) {
    result += ((C) o).hashCode();
} else if (o instanceof D) {
    result += ((D) o).hashCode();
} else { // Actually impossible, but let play it safe.
    result += o.hashCode();
}

Обратите внимание, что компилятор избегает таких оптимизаций для более чем двух реализаций, поскольку большинство вызовов методов намного дороже, чем простая полевая нагрузка, и коэффициент усиления будет мал по сравнению с раздуванием кода.

Исходный вопрос "Почему JIT не оптимизирует hashCode, как и другие методы", остается и hashCode2 доказательств, которые он действительно мог бы сделать.

ОБНОВЛЕНИЕ 2

Похоже, что bestsss прав, по крайней мере с этой записью

вызов hashCode() любого расширяемого класса Base - это то же самое, что и вызов Object.hashCode(), и это то, как он компилируется в байт-коде, если вы добавите явный хэш-код в Base, который ограничит потенциальные цели вызова, ссылающиеся на Base. хэш-код().

Я не совсем уверен в том, что происходит, но объявление Base.hashCode() снова делает конкуренцию hashCode.

results2

ОБНОВЛЕНИЕ 3

ОК, поэтому конкретная реализация Base#hashCode помогает, однако, JIT должен знать, что он никогда не вызывается, поскольку все подклассы определяют свои собственные (если другой подкласс не загружен, что может привести к деоптимизации, но это ничего нового для JIT).

Таким образом, это выглядит как пропущенная вероятность оптимизации # 1.

Обеспечение абстрактной реализации Base#hashCode работает одинаково. Это имеет смысл, поскольку он обеспечивает, что дальнейший поиск не требуется, поскольку каждый подкласс должен предоставлять свои собственные (они не могут просто наследовать их дедушку и бабушку).

Все еще для более чем двух реализаций, myCode намного быстрее, что компилятор должен делать что-то слишком многообразное. Может быть, упущенная вероятность оптимизации №2?

4b9b3361

Ответ 1

hashCode определяется в java.lang.Object, поэтому определение его в вашем собственном классе не делает вообще ничего. (все же это определенный метод, но это не имеет значения)

JIT имеет несколько способов оптимизации сайтов вызовов (в данном случае hashCode()):

  • no overrides - статический вызов (вообще не виртуальный) - наилучший сценарий с полной оптимизацией
  • 2 сайта - ByteBuffer, например: точная проверка типа, а затем статическая отправка. Проверка типа очень проста, но в зависимости от использования, которое оно может или не может быть предсказано аппаратным обеспечением.
  • встроенные кэши - когда в теле вызывающего абонента используется несколько экземпляров класса, возможно, они также встроены в них - что некоторые методы могут быть встроены, некоторые из них могут быть вызваны через виртуальные таблицы. Внутренний бюджет не очень высок. Это в точности соответствует вопросу - другой метод, не названный hashCode(), будет содержать встроенные кеши, поскольку вместо v-table существует только четыре реализации,
  • Добавление большего количества классов, проходящих через это тело вызывающего, приводит к реальному виртуальному вызову, когда компилятор отказывается.

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

Обратите внимание: вызов hashCode() любого класса, расширяющего Base, совпадает с вызовом Object.hashCode(), и это то, как он компилируется в байт-коде, если вы добавите явный хэш-код в базе, который ограничить потенциальные цели вызова, вызывающие Base.hashCode().

Слишком много классов (в самом JDK) имеют hashCode(), переопределенные, поэтому в случаях, когда нестроенные структуры HashMap аналогичны, вызов выполняется через vtable - то есть медленный.

В качестве дополнительного бонуса: при загрузке новых классов JIT должен деоптимизировать существующие сайты вызовов.


Я могу попытаться найти некоторые источники, если кто-то заинтересован в дальнейшем чтении

Ответ 3

Я могу подтвердить выводы. См. Эти результаты (перекомпилирование опущено):

$ /extra/JDK8u5/jdk1.8.0_05/bin/java Main
overCode :    14.135000000s
hashCode :    14.097000000s

$ /extra/JDK7u21/jdk1.7.0_21/bin/java Main
overCode :    14.282000000s
hashCode :    54.210000000s

$ /extra/JDK6u23/jdk1.6.0_23/bin/java Main
overCode :    14.415000000s
hashCode :   104.746000000s

Результаты получены путем многократного вызова методов класса SubA extends Base. Метод overCode() идентичен hashCode(), оба из которых возвращают только поле int.

Теперь интересная часть: если к классу Base

добавлен следующий метод:
@Override
public int hashCode(){
    return super.hashCode();
}

время выполнения для hashCode больше не отличается от параметров overCode.

Base.java:

public class Base {
private int code;
public Base( int x ){
    code = x;
}
public int overCode(){
return code;
}
}

SubA.java:

public class SubA extends Base {
private int code;
public SubA( int x ){
super( 2*x );
    code = x;
}

@Override
public int overCode(){
return code;
}

@Override
public int hashCode(){
    return super.hashCode();
}
}

Ответ 4

Я смотрел на ваши инварианты для вашего теста. У него scenario.vmSpec.options.hashCode установлено значение 0. Согласно это слайд-шоу (слайд 37), что означает, что Object.hashCode будет использовать генератор случайных чисел. Возможно, поэтому компилятор JIT меньше интересуется оптимизацией вызовов hashCode, поскольку он считает вероятным, что ему, возможно, придется прибегать к дорогостоящему вызову метода, что компенсировало бы любые выгоды от повышения эффективности поиска vtable.

Это также может быть связано с тем, что установка Base имеет собственный метод хеш-кода, который повышает производительность, поскольку предотвращает возможность прохода до Object.hashCode.

http://www.slideshare.net/DmitriyDumanskiy/jvm-performance-options-how-it-works

Ответ 5

Семантика hashCode() более сложна, чем обычные методы, поэтому JVM и JIT-компилятор должны делать больше работы при вызове hashCode(), чем при вызове обычного виртуального метода.

Одна специфичность отрицательно влияет на производительность: вызов hashCode() на нулевом объекте действителен и возвращает ноль. Для этого требуется еще одно разветвление, чем обычный вызов, который сам по себе может объяснить разницу в производительности, которую вы создали.

Обратите внимание, что это правда, это похоже только на Java 7 из-за введения Object.hashCode(target), который имеет эту семантику. Было бы интересно узнать, на какой версии вы протестировали эту проблему, и если бы у вас было то же самое на Java6, например.

Другая особенность оказывает положительное влияние на производительность: если вы не предоставляете собственную реализацию hasCode(), компилятор JIT будет использовать встроенный код вычисления hashcode, который быстрее, чем обычный скомпилированный вызов Object.hashCode.

Е.