Почему возврат ссылки на объект Java намного медленнее, чем возврат примитива
мы работаем над чувствительным к задержке приложением и были microbenchmarking все виды методов (используя jmh). После microbenchmarking метод поиска и будучи удовлетворенным результатами, я реализовал окончательную версию, только чтобы найти, что окончательная версия была в 3 раза медленнее чем то, что я только что проверил.
виновником было то, что реализованный метод возвращал enum
объект вместо int
. Вот упрощенная версия кода бенчмарка:
@OutputTimeUnit(TimeUnit.MICROSECONDS)
@State(Scope.Thread)
public class ReturnEnumObjectVersusPrimitiveBenchmark {
enum Category {
CATEGORY1,
CATEGORY2,
}
@Param( {"3", "2", "1" })
String value;
int param;
@Setup
public void setUp() {
param = Integer.parseInt(value);
}
@Benchmark
public int benchmarkReturnOrdinal() {
if (param < 2) {
return Category.CATEGORY1.ordinal();
}
return Category.CATEGORY2.ordinal();
}
@Benchmark
public Category benchmarkReturnReference() {
if (param < 2) {
return Category.CATEGORY1;
}
return Category.CATEGORY2;
}
public static void main(String[] args) throws RunnerException {
Options opt = new OptionsBuilder().include(ReturnEnumObjectVersusPrimitiveBenchmark.class.getName()).warmupIterations(5)
.measurementIterations(4).forks(1).build();
new Runner(opt).run();
}
}
результаты теста Для выше:
# VM invoker: C:Program FilesJavajdk1.7.0_40jrebinjava.exe
# VM options: -Dfile.encoding=UTF-8
Benchmark (value) Mode Samples Score Error Units
benchmarkReturnOrdinal 3 thrpt 4 1059.898 ± 71.749 ops/us
benchmarkReturnOrdinal 2 thrpt 4 1051.122 ± 61.238 ops/us
benchmarkReturnOrdinal 1 thrpt 4 1064.067 ± 90.057 ops/us
benchmarkReturnReference 3 thrpt 4 353.197 ± 25.946 ops/us
benchmarkReturnReference 2 thrpt 4 350.902 ± 19.487 ops/us
benchmarkReturnReference 1 thrpt 4 339.578 ± 144.093 ops/us
просто изменение типа возврата функции изменило производительность почти в 3 раза.
Я думал, что единственное различие между возвращением объекта enum и целым числом заключается в том, что один возвращает 64-битное значение (ссылка), а другой возвращает 32-битное значение. Один из моих коллег предполагал, что возвращение перечисления добавило дополнительные накладные расходы из-за необходимости отслеживать на ГК. (Но учитывая, что объекты enum являются статическими конечными ссылками, кажется странным, что это нужно будет сделать).
каково объяснение разницы в производительности?
обновление
я поделился проектом maven здесь так что любой может клонировать его и запустить тест. Если у кого-то есть время/интерес, было бы полезно посмотреть, есть ли другие можно повторить те же результаты. (Я реплицировал на 2 разных машинах, Windows 64 и Linux 64, оба используя ароматы Oracle Java 1.7 JVMs). @ZhekaKozlov говорит, что не видит никакой разницы между методами.
для запуска: (после клонирования репозитория)
mvn clean install
java -jar .targetmicrobenchmarks.jar function.ReturnEnumObjectVersusPrimitiveBenchmark -i 5 -wi 5 -f 1
2 ответа:
TL; DR: вы не должны слепо доверять ничему.
прежде всего: важно проверить экспериментальные данные, прежде чем перейти к выводам из них. Просто утверждать, что что-то в 3 раза быстрее/медленнее, странно, потому что вам действительно нужно следить за причиной разницы в производительности, а не просто доверять цифрам. Это особенно важно для таких нано-бенчмарков, как у вас.
во-вторых, экспериментаторы должны четко понимать, что они контролируют и что они не. В вашем конкретном примере, вы возвращаете значение из
@Benchmark
методы, но можете ли вы быть уверены, что вызывающие снаружи будут делать то же самое для примитива и ссылки? Если вы зададите себе этот вопрос, то поймете, что вы в основном измеряете тестовую инфраструктуру.вплоть до сути. На моей машине (i5-4210U, Linux x86_64, JDK 8u40) тест дает:
Benchmark (value) Mode Samples Score Error Units ...benchmarkReturnOrdinal 3 thrpt 5 0.876 ± 0.023 ops/ns ...benchmarkReturnOrdinal 2 thrpt 5 0.876 ± 0.009 ops/ns ...benchmarkReturnOrdinal 1 thrpt 5 0.832 ± 0.048 ops/ns ...benchmarkReturnReference 3 thrpt 5 0.292 ± 0.006 ops/ns ...benchmarkReturnReference 2 thrpt 5 0.286 ± 0.024 ops/ns ...benchmarkReturnReference 1 thrpt 5 0.293 ± 0.008 ops/ns
Итак, эталонные тесты появляются в 3 раза медленнее. Но подождите, он использует старый JMH (1.1.1), давайте обновим до текущего последнего (1.7.1):
Benchmark (value) Mode Cnt Score Error Units ...benchmarkReturnOrdinal 3 thrpt 5 0.326 ± 0.010 ops/ns ...benchmarkReturnOrdinal 2 thrpt 5 0.329 ± 0.004 ops/ns ...benchmarkReturnOrdinal 1 thrpt 5 0.329 ± 0.004 ops/ns ...benchmarkReturnReference 3 thrpt 5 0.288 ± 0.005 ops/ns ...benchmarkReturnReference 2 thrpt 5 0.288 ± 0.005 ops/ns ...benchmarkReturnReference 1 thrpt 5 0.288 ± 0.002 ops/ns
Ой, теперь они только чуть медленнее. Кстати, это также говорит нам, что тест связан с инфраструктурой. Хорошо, мы можем посмотреть, что происходит на самом деле?
если вы строите ориентиры и посмотрите вокруг, что именно вызывает
@Benchmark
методы, то вы увидите что-то вроде:public void benchmarkReturnOrdinal_thrpt_jmhStub(InfraControl control, RawResults result, ReturnEnumObjectVersusPrimitiveBenchmark_jmh l_returnenumobjectversusprimitivebenchmark0_0, Blackhole_jmh l_blackhole1_1) throws Throwable { long operations = 0; long realTime = 0; result.startTime = System.nanoTime(); do { l_blackhole1_1.consume(l_longname.benchmarkReturnOrdinal()); operations++; } while(!control.isDone); result.stopTime = System.nanoTime(); result.realTime = realTime; result.measuredOps = operations; }
это
l_blackhole1_1
естьconsume
метод, который "потребляет" значения (см.Blackhole
обоснование).Blackhole.consume
имеет перегрузки для ссылки и примитивы и этого одного достаточно, чтобы оправдать разницу в производительности.есть объяснение, почему эти методы выглядят по-разному: они пытаются быть как можно быстрее для своих типов аргументов. Они не обязательно демонстрируют одинаковые характеристики производительности, даже если мы пытаемся их сопоставить, следовательно, более симметричный результат с более новым JMH. Сейчас, вы даже можете пойти в
-prof perfasm
чтобы увидеть сгенерированный код для тестов и понять, почему производительность отличается, но это не суть.если вы действительно хочу чтобы понять, как возврат примитива и / или ссылки отличается по производительности, вам нужно будет ввести большая страшная серая зона тонкого бенчмаркинга производительности. Например, что-то вроде этого теста:
@BenchmarkMode(Mode.AverageTime) @OutputTimeUnit(TimeUnit.NANOSECONDS) @Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) @Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) @Fork(5) public class PrimVsRef { @Benchmark public void prim() { doPrim(); } @Benchmark public void ref() { doRef(); } @CompilerControl(CompilerControl.Mode.DONT_INLINE) private int doPrim() { return 42; } @CompilerControl(CompilerControl.Mode.DONT_INLINE) private Object doRef() { return this; } }
...что дает тот же результат для примитивов и ссылок:
Benchmark Mode Cnt Score Error Units PrimVsRef.prim avgt 25 2.637 ± 0.017 ns/op PrimVsRef.ref avgt 25 2.634 ± 0.005 ns/op
как я уже сказал выше, эти тесты требуются выяснение причин полученных результатов. В этом случае сгенерированный код для обоих почти одинаков, и это объясняет результат.
прим:
[Verified Entry Point] 12.69% 1.81% 0x00007f5724aec100: mov %eax,-0x14000(%rsp) 0.90% 0.74% 0x00007f5724aec107: push %rbp 0.01% 0.01% 0x00007f5724aec108: sub x30,%rsp 12.23% 16.00% 0x00007f5724aec10c: mov x2a,%eax ; load "42" 0.95% 0.97% 0x00007f5724aec111: add x30,%rsp 0.02% 0x00007f5724aec115: pop %rbp 37.94% 54.70% 0x00007f5724aec116: test %eax,0x10d1aee4(%rip) 0.04% 0.02% 0x00007f5724aec11c: retq
ref:
[Verified Entry Point] 13.52% 1.45% 0x00007f1887e66700: mov %eax,-0x14000(%rsp) 0.60% 0.37% 0x00007f1887e66707: push %rbp 0.02% 0x00007f1887e66708: sub x30,%rsp 13.63% 16.91% 0x00007f1887e6670c: mov %rsi,%rax ; load "this" 0.50% 0.49% 0x00007f1887e6670f: add x30,%rsp 0.01% 0x00007f1887e66713: pop %rbp 39.18% 57.65% 0x00007f1887e66714: test %eax,0xe3e78e6(%rip) 0.02% 0x00007f1887e6671a: retq
[сарказм] смотрите, как это просто! [/сарказм]
шаблон: чем проще вопрос, тем больше вам придется работать, чтобы сделать обоснованный и достоверный ответ.
чтобы очистить заблуждение ссылка и некоторые из них попали в (@Mzf), давайте погрузимся в спецификацию виртуальной машины Java. Но прежде чем идти туда, нужно прояснить одну вещь -объект никогда не может быть извлечен из памяти, только его поля могут. На самом деле, нет никакого кода операции, который бы выполнял такую обширную операцию.
этот документ определяет ссылку как тип стека (так что это может быть результат или аргумент к инструкциям, выполняющим операции над стеком) 1 - й категории-категории типов, принимающих одно слово стека (32 бита). См. таблицу 2.3 .
кроме того, если вызов метода завершается нормально в соответствии со спецификацией, значение, выскочившее из верхней части стека, помещается в стек вызова методов (раздел 2.6.4).
Ваш вопрос заключается в том, что вызывает разницу во времени выполнения. Глава 2 предисловие ответы:
детали реализации, которые не являются частью спецификации виртуальной машины Java излишне ограничили бы творческий потенциал конструкторов. Например, компоновка памяти областей данных во время выполнения, используемый алгоритм сбора мусора и любая внутренняя оптимизация инструкций виртуальной машины Java (например, перевод их в машинный код) оставляются на усмотрение исполнителя.
в других слова, потому что никакая такая вещь, как штраф performace относительно использования ссылки, не указана в документе по логическим причинам (в конечном итоге это просто слово стека как
int
илиfloat
are), вы остаетесь с поиском исходного кода вашей реализации или никогда не узнаете вообще.в степени, мы не должны на самом деле всегда обвинять реализацию, есть некоторые подсказки, которые вы можете взять при поиске ваших ответов. Java определяет отдельные инструкции для работы с числами и ссылки. Справочно-манипулирующие инструкции начинаются с
a
(например,astore
,aload
илиareturn
) и являются единственными инструкциями, разрешенными для работы со ссылками. В частности, вам может быть интересно посмотреть наareturn
реализация s.