Эффективный возврат двух значений из функции Java

Кто-нибудь знает, есть ли способ вернуть два значения из Java с (близкими) нулевыми накладными расходами? Я ищу только два значения - у меня есть пара вариантов использования: от обработки массива байтов (и мне нужно возвращаемое значение и следующая начальная позиция) до попытки вернуть значение с кодом ошибки, чтобы сделать какое-то уродство с фиксированным- точечные расчеты и нужна целая и дробная часть.

Я не ниже некоторых действительно уродливых хаков. Функция небольшая, и Hotspot с радостью встраивает ее. Так что теперь мне просто нужно, чтобы Hotspot в основном исключал любое создание объекта или сдвиг битов.

Если я ограничу возвращаемые значения целыми числами, я попытался упаковать их в длинный, но даже после встраивания Hotspot не может понять, что все битовые сдвиги и маски на самом деле ничего не делают, и он с радостью упаковывает и распаковывает ints в одни и те же значения (очевидно, здесь нужна помощь оптимизатору глазка Hotspot). Но по крайней мере я не создаю объект.

Мой более сложный случай, когда один из элементов, которые мне нужно вернуть, является ссылкой, а другой — длинной или другой ссылкой (для случая int я думаю, что могу сжать ООП и использовать битовую упаковку, описанную выше).

Кто-нибудь пытался заставить Hotspot генерировать для этого код без мусора? В худшем случае сейчас мне нужно носить с собой объект и передавать его, но я бы хотел, чтобы он был автономным. Локальные потоки дороги (поиск хэшей), и они должны быть реентерабельными.


person JasonN    schedule 17.11.2015    source источник
comment
объединить их в строку с фиксированным разделителем, создать составной объект, который вы возвращаете, вернуть набор объектов,...   -  person Stultuske    schedule 17.11.2015
comment
@Stultuske все эти подходы генерируют мусор, особенно объединяя ваши результаты в строки.   -  person Louis Wasserman    schedule 17.11.2015
comment
Длинный выстрел (вроде), но вы не думали вообще ничего не возвращать? Вы можете всегда извлекать код после вызова (якобы это какое-то сопоставление или накопление логика). Хотя все, вероятно, согласны с тем, что HotSpot должен быть в состоянии спасти положение в любом случае, простое представление логики по-другому может подтолкнуть оптимизатора в правильном направлении. (Очевидно, что вы не написали бы сам цикл в CPS или вы бы взорвали стек; только код обработки отдельных элементов и обработка результатов.)   -  person tne    schedule 18.11.2015
comment
@tne В некоторых случаях он отображается, в других случаях (например, при выполнении фиксированных вычислений и необходимости передать обратно как значение, так и количество десятичных разрядов) это не так. Но я заинтригован твоей идеей. Не могли бы вы рассказать мне немного конкретнее? Что бы я превратил в CPS?   -  person JasonN    schedule 18.11.2015
comment
Вам, вероятно, потребуется опубликовать отдельный вопрос с конкретным кодом, чтобы получить полезный ответ, но в целом: извлеките код после вызова и до конца цикла в отдельный класс. Вы захотите поместить сопоставленный набор данных или накопитель в член поля, чтобы получить его позже. Затем вы вводите экземпляр этого класса в объект целевого метода. Вместо того, чтобы возвращать результаты, метод, в свою очередь, вызовет внедренный код с результатами в качестве аргументов и ничего не вернет (void). Как только вы выходите из цикла, вы получаете результаты во внедренном объекте от вызывающего объекта.   -  person tne    schedule 19.11.2015


Ответы (3)


Оптимизация -XX:+EliminateAllocations (включена по умолчанию в Java 8) отлично подходит для этого.

Всякий раз, когда вы возвращаете new Pair(a, b) прямо в конце метода вызываемого объекта и сразу же используете результат в вызывающем объекте, JVM, скорее всего, выполнит скалярную замену, если вызываемый объект встроен.

Простой эксперимент показывает, что при возврате объекта почти нет накладных расходов. Это не только эффективный способ, но и самый читаемый.

Benchmark                        Mode  Cnt    Score   Error   Units
ReturnPair.manualInline         thrpt   30  127,713 ± 3,408  ops/us
ReturnPair.packToLong           thrpt   30  113,606 ± 1,807  ops/us
ReturnPair.pairObject           thrpt   30  126,881 ± 0,478  ops/us
ReturnPair.pairObjectAllocated  thrpt   30   92,477 ± 0,621  ops/us

Эталон:

import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.infra.Blackhole;

import java.util.concurrent.ThreadLocalRandom;

@State(Scope.Benchmark)
public class ReturnPair {
    int counter;

    @Benchmark
    public void manualInline(Blackhole bh) {
        bh.consume(counter++);
        bh.consume(ThreadLocalRandom.current().nextInt());
    }

    @Benchmark
    public void packToLong(Blackhole bh) {
        long packed = getPacked();
        bh.consume((int) (packed >>> 32));
        bh.consume((int) packed);
    }

    @Benchmark
    public void pairObject(Blackhole bh) {
        Pair pair = getPair();
        bh.consume(pair.a);
        bh.consume(pair.b);
    }

    @Benchmark
    @Fork(jvmArgs = "-XX:-EliminateAllocations")
    public void pairObjectAllocated(Blackhole bh) {
        Pair pair = getPair();
        bh.consume(pair.a);
        bh.consume(pair.b);
    }

    public long getPacked() {
        int a = counter++;
        int b = ThreadLocalRandom.current().nextInt();
        return (long) a << 32 | (b & 0xffffffffL);
    }

    public Pair getPair() {
        int a = counter++;
        int b = ThreadLocalRandom.current().nextInt();
        return new Pair(a, b);
    }

    static class Pair {
        final int a;
        final int b;

        Pair(int a, int b) {
            this.a = a;
            this.b = b;
        }
    }
}
person apangin    schedule 18.11.2015
comment
Если это не ново, я попробовал это в Java 8 (я думаю, около микроверсии 20), и просмотр дампов сборки JMH показал, что создание объекта не исключалось. Может быть, мне просто нужно было быть более понятным для Hotspot (например, убедиться, что возврат создает объект со значениями ctor вместо того, чтобы создавать его в верхней части func и заполнять переменные экземпляра). Хотя я обязательно попробую еще раз. Вы случайно не смотрели сборку? - person JasonN; 18.11.2015
comment
Проходит ли вообще использование final help в финальной компиляции? (Насколько мне известно, это помогает на более ранних уровнях, но кажется, что последний уровень всегда занимает одно и то же место без финала). - person JasonN; 18.11.2015
comment
Я на самом деле шокирован тем, что Hotspot не может понять, что упаковка/распаковка является NOP после встраивания. - person JasonN; 18.11.2015
comment
@JasonN Да, я просмотрел сгенерированную сборку, чтобы убедиться, что выделение существует только в последнем тесте. Модификатор final для нестатических полей обычно не влияет на производительность, включая этот конкретный случай. Но я думаю, что в любом случае это хорошая практика — помечать поля как final, чтобы указать, что такие классы используются больше как типы значений. - person apangin; 19.11.2015

Описанное вами решение почти настолько же хорошо, как вы можете получить в Hotspot - передача объекта для хранения возвращаемых значений и его изменение. (Типы значений Java 10 могли бы сделать здесь что-то лучше, но я не думаю, что это еще даже на стадии прототипа.)

Тем не менее, небольшие короткоживущие объекты на самом деле не так уж далеки от нулевых накладных расходов. Сборка мусора из недолговечных объектов преднамеренно очень дешева.

person Louis Wasserman    schedule 17.11.2015
comment
Я на самом деле шокирован тем, что упаковка целых чисел в длинное, а затем распаковка не оптимизируется после того, как функция будет встроена. Когда я посмотрел дамп сборки с помощью JMH, он буквально сдвигает и или значения вместе, сразу же снимает сдвиг и маскирует. Иногда Hotspot — это волшебство, а иногда — отстой. К тому же недостаточно дешево. Хотя потенциально это просто удар TLB, он может быть намного больше, а также имеет нулевые накладные расходы. - person JasonN; 17.11.2015
comment
@JasonN, если вы можете это сделать, смещение и снятие смещения, вероятно, все равно будет дешевле, чем выделение объектов, так что это будет ваш лучший вариант. - person Louis Wasserman; 17.11.2015
comment
Это определенно так. Просто в некоторых случаях возврата я не могу найти способ сделать это. Сжатие двух oops и упаковка их в long, а затем распаковка и распаковка другой стороны также выполнимы, но все еще требуют около 20 инструкций (по крайней мере). Не говоря уже о том, что нет никакого способа получить библиотеку из этого кода, поскольку код библиотеки, конечно, должен возвращать два значения :) - person JasonN; 17.11.2015

Мне пришлось столкнуться с этой проблемой, и я обнаружил, что лучший способ — создать экземпляр простого final class с public полями, а затем передать его в качестве параметра вашему методу. Отправьте результаты в этот экземпляр.

Если у вас есть цикл, попробуйте повторно использовать этот экземпляр как можно дольше.

Наличие сеттеров и геттеров в Java 7 (когда я это делал) имело очень небольшие накладные расходы. То же самое происходит с созданием новых объектов в каждом цикле.

person Guillaume F.    schedule 17.11.2015
comment
Геттеры и сеттеры (которые никогда не переопределяются) должны иметь нулевые накладные расходы после того, как Hotspot завершит их (что может произойти только после последнего прохода компиляции при использовании уровней). Он счастливо убирает их. Тем не менее, я никогда не использую геттеры и сеттеры (я всегда преобразовываю их в защищенные или общедоступные). - person JasonN; 17.11.2015