Головоломка производительности Java: классы-оболочки быстрее, чем примитивные типы?

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

В приведенном ниже примере я определил два типа посетителей.

  • примитивный тип, где сигнатура метода visitvisit(int, int double)
  • универсальный тип, где сигнатура метода visitvisit(int, int Double).

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

Итак, вот полная программа

public class VisitorsBenchmark {
    public interface Array2DGenericVisitor<TYPE, RET> {

        void begin(int width, int height);

        RET end();

        void visit(int x, int y, TYPE value);
    }

    public interface Array2DPrimitiveVisitor<RET> {

        void begin(final int width, final int height);

        RET end();

        void visit(final int x, final int y, final double value);
    }

    public static <RET>
        RET
        accept(final int width,
               final int height,
               final double[] data,
               final Array2DGenericVisitor<Double, RET> visitor) {

        final int size = width * height;
        visitor.begin(width, height);
        for (int i = 0, x = 0, y = 0; i < size; i++) {
            visitor.visit(x, y, data[i]);
            x++;
            if (x == width) {
                x = 0;
                y++;
                if (y == height) {
                    y = 0;
                }
            }
        }
        return visitor.end();
    }

    public static <RET> RET accept(final int width,
                                   final int height,
                                   final double[] data,
                                   final Array2DPrimitiveVisitor<RET> visitor) {

        final int size = width * height;
        visitor.begin(width, height);
        for (int i = 0, x = 0, y = 0; i < size; i++) {
            visitor.visit(x, y, data[i]);
            x++;
            if (x == width) {
                x = 0;
                y++;
                if (y == height) {
                    y = 0;
                }
            }
        }
        return visitor.end();
    }

    private static final Array2DGenericVisitor<Double, double[]> generic;

    private static final Array2DPrimitiveVisitor<double[]> primitive;

    static {
        generic = new Array2DGenericVisitor<Double, double[]>() {
            private double[] sum;

            @Override
            public void begin(final int width, final int height) {

                final int length = (int) Math.ceil(Math.hypot(WIDTH, HEIGHT));
                sum = new double[length];
            }

            @Override
            public void visit(final int x, final int y, final Double value) {

                final int r = (int) Math.round(Math.sqrt(x * x + y * y));
                sum[r] += value;
            }

            @Override
            public double[] end() {

                return sum;
            }
        };

        primitive = new Array2DPrimitiveVisitor<double[]>() {
            private double[] sum;

            @Override
            public void begin(final int width, final int height) {

                final int length = (int) Math.ceil(Math.hypot(WIDTH, HEIGHT));
                sum = new double[length];
            }

            @Override
            public void visit(final int x, final int y, final double value) {

                final int r = (int) Math.round(Math.sqrt(x * x + y * y));
                sum[r] += value;
            }

            @Override
            public double[] end() {

                return sum;
            }
        };
    }

    private static final int WIDTH = 300;

    private static final int HEIGHT = 300;

    private static final int NUM_ITERATIONS_PREHEATING = 10000;

    private static final int NUM_ITERATIONS_BENCHMARKING = 10000;

    public static void main(String[] args) {

        final double[] data = new double[WIDTH * HEIGHT];
        for (int i = 0; i < data.length; i++) {
            data[i] = Math.random();
        }

        /*
         * Pre-heating.
         */
        for (int i = 0; i < NUM_ITERATIONS_PREHEATING; i++) {
            accept(WIDTH, HEIGHT, data, generic);
        }
        for (int i = 0; i < NUM_ITERATIONS_PREHEATING; i++) {
            accept(WIDTH, HEIGHT, data, primitive);
        }

        /*
         * Benchmarking proper.
         */
        double[] sumPrimitive = null;
        double[] sumGeneric = null;

        double aux = System.nanoTime();
        for (int i = 0; i < NUM_ITERATIONS_BENCHMARKING; i++) {
            sumGeneric = accept(WIDTH, HEIGHT, data, generic);
        }
        final double timeGeneric = System.nanoTime() - aux;

        aux = System.nanoTime();
        for (int i = 0; i < NUM_ITERATIONS_BENCHMARKING; i++) {
            sumPrimitive = accept(WIDTH, HEIGHT, data, primitive);
        }
        final double timePrimitive = System.nanoTime() - aux;

        System.out.println("prim = " + timePrimitive);
        System.out.println("generic = " + timeGeneric);
        System.out.println("generic / primitive = "
                           + (timeGeneric / timePrimitive));
    }
}

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

Спасибо большое за вашу помощь!!! Себастьян

[EDIT] Я обновил код, чтобы учесть фазу предварительного нагрева (чтобы JIT-компилятор мог выполнять свою работу). Это не меняет результатов, которые стабильно ниже 1 (0,95–0,98).


person Sebastien    schedule 10.09.2012    source источник
comment
Передача Primitive double включает копирование 8 байтов в стек. Для передачи Double требуется только копирование указателя.   -  person Paul Tomblin    schedule 10.09.2012
comment
Вы должны поместить измеренные задачи в отдельные методы и запустить их несколько раз, пока они не будут скомпилированы (10 000/15 000 должно быть хорошо). Затем запустите их в цикле и измерьте. Этот пост обязателен к прочтению.   -  person assylias    schedule 10.09.2012
comment
Если я запускаю тест несколько раз, разница составляет от 0,99 до 1,06, а общий немного медленнее.   -  person Peter Lawrey    schedule 10.09.2012
comment
Если я бегу с -mx12m, соотношение будет между 1,03 и 1,14.   -  person Peter Lawrey    schedule 10.09.2012
comment
@Питер: странно! Я постоянно получаю результат от 0,95 до 0,98!   -  person Sebastien    schedule 10.09.2012
comment
@Assylias: хорошее предложение, я соответствующим образом обновил код, но результат остался прежним. Поскольку каждый цикл уже вызывает только один метод, я думаю, мне не нужно создавать отдельные методы.   -  person Sebastien    schedule 10.09.2012
comment
@Пол: интересно! Таким образом, предположительно, выполнение того же теста с byte/Byte должно показать противоположную тенденцию. Попробую.   -  person Sebastien    schedule 10.09.2012
comment
что происходит, когда вы сначала запускаете коробочную версию?   -  person Konstantin Pribluda    schedule 10.09.2012
comment
@Константин: уже пробовал! Все равно быстрее...   -  person Sebastien    schedule 10.09.2012
comment
@PaulTomblin: я запускал ту же программу с Byte/byte вместо Double/double, и примитивная версия теперь немного быстрее (1.0006348844333905). Так что я готов принять ваш комментарий как ответ, но не уверен, что смогу :(   -  person Sebastien    schedule 10.09.2012
comment
опубликуйте JVM и оборудование, на котором вы запускаете тест, а также параметры JVM.   -  person bestsss    schedule 10.09.2012


Ответы (3)


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

Я думаю, что проблема в том, что ваш бенчмаркинг не учитывает прогрев JVM. Поместите тело основного метода и поместите его в другой метод. Затем сделайте так, чтобы ваш метод main неоднократно вызывал этот новый метод в цикле. Наконец, изучите результаты и отбросьте первые несколько, которые искажены JIT-компиляцией и другими эффектами прогрева.

person Stephen C    schedule 10.09.2012

Небольшие советы:

  • Не используйте Math.random() для выполнения тестов, так как результаты недетерминированы. Вам нужно что-то вроде new Random(xxx).
  • Всегда печатайте результат операции. Смешивание типов тестов в одном выполнении — плохая практика, так как это может привести к разным оптимизациям сайта вызова (хотя и не в вашем случае).
  • double aux = System.nanoTime(); -- не все longs вписываются в удвоения - правильно.
  • опубликуйте спецификацию среды и оборудования, на котором вы выполняете тесты
  • напечатайте «стартовый тест» при включенной печати компиляции -XX:-PrintCompilation и сборки мусора -verbosegc -XX:+PrintGCDetails — сборщик мусора может сработать во время «неправильного» теста ровно настолько, чтобы исказить результаты.


Изменить:

Я проверил сгенерированный ассемблер, и ни одна из них не является настоящей причиной. Для Double.valueOf() нет выделения, поскольку метод полностью встроен и оптимизирован — он использует только регистры ЦП. Однако без спецификации оборудования/JVM нет реального ответа.

Я нашел JVM (1.6.0.26), где универсальная версия (Double) имеет лучшее развертывание цикла (!), благодаря более глубокому анализу (очевидно, необходимому для EA Double.valueOf()) и, возможно, постоянное складывание WIDTH/HEIGHT. Измените WIDTH/HEIGHT на некоторые простые числа и результаты должны отличаться.


Суть такова: не используйте микробенчмарки, если вы не знаете, как оптимизируется JVM, и не проверяете сгенерированный машинный код.


Отказ от ответственности: я не инженер JVM

person bestsss    schedule 10.09.2012
comment
Спасибо за эти советы. Я сосредоточусь на последнем, так как не думал об этом вопросе. Однако я не думаю, что это причина такого результата, поскольку изменение порядка двух циклов не меняет результат. - person Sebastien; 11.09.2012
comment
@Себастьян, кажется, я получил твой ответ - person bestsss; 11.09.2012

Это совершенно «дикая догадка», но я думаю, что это связано с копированием байтов в стек. Передача примитивного двойника включает копирование 8 байтов в стек. Для передачи Double требуется только копирование указателя.

person Paul Tomblin    schedule 10.09.2012
comment
это не может быть правдой - метод является одним сайтом вызова, то есть статическим - JVM обязательно его встроит. - person bestsss; 10.09.2012
comment
Если это не так, то почему с byte он быстрее, чем с Byte, но медленнее с double, чем с Double? - person Paul Tomblin; 10.09.2012
comment
Проверил сгенерированную сборку (-server -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly) - оба метода абсолютно встроены, а Double.valueOf() опущен (т.е. его вообще нет). Bytes.valueOf() никогда не выделяется между прочим и всегда кэшируется. - person bestsss; 10.09.2012
comment
Это, безусловно, интересный момент. Пол, я был уверен, что у тебя есть ответ, но бестссс определенно прав, не так ли? Я буду продолжать думать об этом ... В любом случае, действительно важный ответ для меня заключается в том, что бокс не имеет большого значения с точки зрения времени. Мне будет намного проще сохранить общую версию посетителя, так как я хочу иметь дело с byte[], float[], long[] и так далее. - person Sebastien; 11.09.2012
comment
@Sebastien, BOXing имеет значение, если его нельзя встроить, то есть иметь более одного (на самом деле 2) класса, реализующего интерфейс, и вы увидите огромную разницу. После того, как вы начнете иметь 3, которые можно относительно использовать с одинаковой частотой, вы увидите огромное влияние, поскольку больше не будет встраивания и оптимизации сайта вызова. Теперь вам нужно тривиально оптимизировать тестовый пример — и по этой причине не использовать микробенчмарки, они ложь. - person bestsss; 11.09.2012
comment
Жаль, что я увидел этот комментарий только сейчас... Я часто использую Number.doubleValue(). В этом случае (когда я использую интерфейс) я должен увидеть потерю производительности, если я правильно понимаю. Позор, это путь, которым я следовал... - person Sebastien; 18.09.2012