Почему 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?
Ответы
Ответ 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 должен деоптимизировать существующие сайты вызовов.
Я могу попытаться найти некоторые источники, если кто-то заинтересован в дальнейшем чтении
Ответ 2
Это известная проблема с производительностью:
https://bugs.openjdk.java.net/browse/JDK-8014447
Он был исправлен в JDK 8.
Ответ 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.
Е.