Почему long медленнее, чем int в x64 Java?

92

Я использую Windows 8.1 x64 с обновлением Java 7 45 x64 (32-разрядная версия Java не установлена) на планшете Surface Pro 2.

Приведенный ниже код занимает 1688 мсек, если тип i является длинным, и 109 мсек, когда я является целым. Почему long (64-битный тип) на порядок медленнее, чем int на 64-битной платформе с 64-битной JVM?

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

Я запускаю это в Eclipse Kepler SR1, кстати.

public class Main {

    private static long i = Integer.MAX_VALUE;

    public static void main(String[] args) {    
        System.out.println("Starting the loop");
        long startTime = System.currentTimeMillis();
        while(!decrementAndCheck()){
        }
        long endTime = System.currentTimeMillis();
        System.out.println("Finished the loop in " + (endTime - startTime) + "ms");
    }

    private static boolean decrementAndCheck() {
        return --i < 0;
    }

}

Изменить: вот результаты эквивалентного кода C ++, скомпилированного VS 2013 (ниже), той же системы. длинный: 72265 мс int: 74656 мс Эти результаты были в 32-битном режиме отладки.

В 64-битном режиме выпуска: длинный: 875 мс long long: 906 мс int: 1047 мс

Это говорит о том, что результатом, который я наблюдал, является странность оптимизации JVM, а не ограничения ЦП.

#include "stdafx.h"
#include "iostream"
#include "windows.h"
#include "limits.h"

long long i = INT_MAX;

using namespace std;


boolean decrementAndCheck() {
return --i < 0;
}


int _tmain(int argc, _TCHAR* argv[])
{


cout << "Starting the loop" << endl;

unsigned long startTime = GetTickCount64();
while (!decrementAndCheck()){
}
unsigned long endTime = GetTickCount64();

cout << "Finished the loop in " << (endTime - startTime) << "ms" << endl;



}

Изменить: просто попробовал это еще раз в Java 8 RTM, никаких существенных изменений.

Techrocket9
источник
8
Наиболее вероятным подозрением является ваша установка, а не процессор или различные части JVM. Можете ли вы достоверно воспроизвести это измерение? Не повторять цикл, не разогревать JIT, не использовать currentTimeMillis()исполняемый код, который можно тривиально оптимизировать полностью, и т. Д. Пахнет ненадежными результатами.
1
Некоторое время назад я проводил тестирование, мне пришлось использовать a longв качестве счетчика цикла, потому что компилятор JIT оптимизировал цикл, когда я использовал int. Нужно будет посмотреть на дизассемблирование сгенерированного машинного кода.
Сэм
7
Это неправильный микробенчмарк, и я не ожидал, что его результаты каким-либо образом отражают реальность.
Луи Вассерман,
7
Все комментарии, осуждающие OP за неспособность написать надлежащий микробенчмарк Java, несказанно ленивы. Это то, что очень легко понять, если вы просто посмотрите и увидите, что JVM делает с кодом.
tmyklebu 07
2
@maaartinus: Общепринятая практика - это общепринятая практика, потому что она работает вокруг списка известных ловушек. В случае с надлежащими тестами Java вы хотите убедиться, что вы измеряете правильно оптимизированный код, а не замену в стеке, и хотите, чтобы ваши измерения были чистыми в конце. OP обнаружил совершенно другую проблему, и тест, который он предоставил, адекватно продемонстрировал это. И, как уже отмечалось, превращение этого кода в надлежащий тест производительности Java на самом деле не устраняет странности. А читать ассемблерный код не сложно.
tmyklebu 07

Ответы:

82

Моя JVM делает это с внутренним циклом довольно просто, когда вы используете longs:

0x00007fdd859dbb80: test   %eax,0x5f7847a(%rip)  /* fun JVM hack */
0x00007fdd859dbb86: dec    %r11                  /* i-- */
0x00007fdd859dbb89: mov    %r11,0x258(%r10)      /* store i to memory */
0x00007fdd859dbb90: test   %r11,%r11             /* unnecessary test */
0x00007fdd859dbb93: jge    0x00007fdd859dbb80    /* go back to the loop top */

Это жестко обманывает, когда вы используете ints; во-первых, есть некоторая странность, которую я не понимаю, но похоже на настройку для развернутого цикла:

0x00007f3dc290b5a1: mov    %r11d,%r9d
0x00007f3dc290b5a4: dec    %r9d
0x00007f3dc290b5a7: mov    %r9d,0x258(%r10)
0x00007f3dc290b5ae: test   %r9d,%r9d
0x00007f3dc290b5b1: jl     0x00007f3dc290b662
0x00007f3dc290b5b7: add    $0xfffffffffffffffe,%r11d
0x00007f3dc290b5bb: mov    %r9d,%ecx
0x00007f3dc290b5be: dec    %ecx              
0x00007f3dc290b5c0: mov    %ecx,0x258(%r10)   
0x00007f3dc290b5c7: cmp    %r11d,%ecx
0x00007f3dc290b5ca: jle    0x00007f3dc290b5d1
0x00007f3dc290b5cc: mov    %ecx,%r9d
0x00007f3dc290b5cf: jmp    0x00007f3dc290b5bb
0x00007f3dc290b5d1: and    $0xfffffffffffffffe,%r9d
0x00007f3dc290b5d5: mov    %r9d,%r8d
0x00007f3dc290b5d8: neg    %r8d
0x00007f3dc290b5db: sar    $0x1f,%r8d
0x00007f3dc290b5df: shr    $0x1f,%r8d
0x00007f3dc290b5e3: sub    %r9d,%r8d
0x00007f3dc290b5e6: sar    %r8d
0x00007f3dc290b5e9: neg    %r8d
0x00007f3dc290b5ec: and    $0xfffffffffffffffe,%r8d
0x00007f3dc290b5f0: shl    %r8d
0x00007f3dc290b5f3: mov    %r8d,%r11d
0x00007f3dc290b5f6: neg    %r11d
0x00007f3dc290b5f9: sar    $0x1f,%r11d
0x00007f3dc290b5fd: shr    $0x1e,%r11d
0x00007f3dc290b601: sub    %r8d,%r11d
0x00007f3dc290b604: sar    $0x2,%r11d
0x00007f3dc290b608: neg    %r11d
0x00007f3dc290b60b: and    $0xfffffffffffffffe,%r11d
0x00007f3dc290b60f: shl    $0x2,%r11d
0x00007f3dc290b613: mov    %r11d,%r9d
0x00007f3dc290b616: neg    %r9d
0x00007f3dc290b619: sar    $0x1f,%r9d
0x00007f3dc290b61d: shr    $0x1d,%r9d
0x00007f3dc290b621: sub    %r11d,%r9d
0x00007f3dc290b624: sar    $0x3,%r9d
0x00007f3dc290b628: neg    %r9d
0x00007f3dc290b62b: and    $0xfffffffffffffffe,%r9d
0x00007f3dc290b62f: shl    $0x3,%r9d
0x00007f3dc290b633: mov    %ecx,%r11d
0x00007f3dc290b636: sub    %r9d,%r11d
0x00007f3dc290b639: cmp    %r11d,%ecx
0x00007f3dc290b63c: jle    0x00007f3dc290b64f
0x00007f3dc290b63e: xchg   %ax,%ax /* OK, fine; I know what a nop looks like */

затем сам развернутый цикл:

0x00007f3dc290b640: add    $0xfffffffffffffff0,%ecx
0x00007f3dc290b643: mov    %ecx,0x258(%r10)
0x00007f3dc290b64a: cmp    %r11d,%ecx
0x00007f3dc290b64d: jg     0x00007f3dc290b640

затем код разрыва для развернутого цикла, сам по себе тест и прямой цикл:

0x00007f3dc290b64f: cmp    $0xffffffffffffffff,%ecx
0x00007f3dc290b652: jle    0x00007f3dc290b662
0x00007f3dc290b654: dec    %ecx
0x00007f3dc290b656: mov    %ecx,0x258(%r10)
0x00007f3dc290b65d: cmp    $0xffffffffffffffff,%ecx
0x00007f3dc290b660: jg     0x00007f3dc290b654

Таким образом, для целых чисел это происходит в 16 раз быстрее, потому что JIT развернул intцикл 16 раз, но не развернул longцикл вообще.

Для полноты, вот код, который я на самом деле пробовал:

public class foo136 {
  private static int i = Integer.MAX_VALUE;
  public static void main(String[] args) {
    System.out.println("Starting the loop");
    for (int foo = 0; foo < 100; foo++)
      doit();
  }

  static void doit() {
    i = Integer.MAX_VALUE;
    long startTime = System.currentTimeMillis();
    while(!decrementAndCheck()){
    }
    long endTime = System.currentTimeMillis();
    System.out.println("Finished the loop in " + (endTime - startTime) + "ms");
  }

  private static boolean decrementAndCheck() {
    return --i < 0;
  }
}

Сборочные дампы были созданы с использованием опций -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly. Обратите внимание, что вам нужно возиться с установкой JVM, чтобы эта работа также работала для вас; вам нужно поместить какую-то случайную разделяемую библиотеку точно в нужное место, иначе она выйдет из строя.

тмыклебу
источник
9
Итак, net-net не означает, что longверсия медленнее, а скорее, чем intверсия быстрее. Это имеет смысл. Вероятно, не так много усилий было вложено в создание longвыражений оптимизации JIT .
Hot Licks
1
... простите за незнание, но что такое "развлекается"? Кажется, я даже не могу правильно погуглить этот термин, и поэтому мне впервые пришлось спрашивать кого-то, что означает слово в Интернете.
BrianH
1
@BrianDHall gccиспользует -fв качестве переключателя командной строки для «флага», и unroll-loopsоптимизация включается, говоря -funroll-loops. Я просто использую слово «развернуть» для описания оптимизации.
chrylis -cautiouslyoptimistic-
4
@BRPocock: компилятор Java не может, но JIT точно может.
tmyklebu
1
Чтобы быть ясным, это не "забавно". Он развернул его И преобразовал развернутый цикл в i-=16, что, конечно, в 16 раз быстрее.
Александр Дубинский
22

Стек JVM определяется словами , размер которых является деталью реализации, но должен быть не менее 32 битов. Разработчик JVM может использовать 64-битные слова, но байт-код не может полагаться на это, поэтому операции со значениями longили doubleдолжны обрабатываться с особой осторожностью. В частности, инструкции целочисленного ветвления JVM определены именно для этого типа int.

В случае вашего кода поучительно дизассемблирование. Вот байт-код для intверсии, скомпилированной Oracle JDK 7:

private static boolean decrementAndCheck();
  Code:
     0: getstatic     #14  // Field i:I
     3: iconst_1      
     4: isub          
     5: dup           
     6: putstatic     #14  // Field i:I
     9: ifge          16
    12: iconst_1      
    13: goto          17
    16: iconst_0      
    17: ireturn       

Обратите внимание, что JVM загрузит значение вашего статического i(0), вычтет единицу (3-4), продублирует значение в стеке (5) и вернет его в переменную (6). Затем он выполняет ветвь сравнения с нулем и возвращается.

Версия с longнемного сложнее:

private static boolean decrementAndCheck();
  Code:
     0: getstatic     #14  // Field i:J
     3: lconst_1      
     4: lsub          
     5: dup2          
     6: putstatic     #14  // Field i:J
     9: lconst_0      
    10: lcmp          
    11: ifge          18
    14: iconst_1      
    15: goto          19
    18: iconst_0      
    19: ireturn       

Во-первых, когда JVM дублирует новое значение в стеке (5), она должна дублировать два слова стека. В вашем случае вполне возможно, что это не дороже, чем дублирование, поскольку JVM может свободно использовать 64-битное слово, если это удобно. Однако вы заметите, что здесь логика ветвления длиннее. В JVM нет инструкции для сравнения a longс нулем, поэтому она должна поместить константу 0Lв стек (9), выполнить общее longсравнение (10), а затем перейти к значению этого вычисления.

Вот два возможных сценария:

  • JVM точно следует пути байт-кода. В этом случае он выполняет больше работы в longверсии, вставляя и выталкивая несколько дополнительных значений, и они находятся в виртуальном управляемом стеке , а не в реальном стеке процессора с аппаратной поддержкой. Если это так, вы все равно увидите значительную разницу в производительности после прогрева.
  • JVM понимает, что может оптимизировать этот код. В этом случае требуется дополнительное время, чтобы оптимизировать часть практически ненужной логики push / compare. Если это так, вы увидите очень небольшую разницу в производительности после прогрева.

Я рекомендую вам написать правильный микробенчмарк, чтобы устранить эффект включения JIT, а также попробовать это с конечным условием, отличным от нуля, чтобы заставить JVM провести такое же сравнение int, что и с long.

Chrylis - осторожно оптимистично -
источник
1
@Katona Не обязательно. В частности, клиентские и серверные JVM HotSpot - это совершенно разные реализации, и Илья не указал на выбор сервера (клиент обычно 32-разрядный по умолчанию).
chrylis -cautiouslyoptimistic-
1
@tmyklebu Проблема в том, что тест измеряет сразу несколько разных вещей. Использование ненулевого терминального условия уменьшает количество переменных.
chrylis -cautiouslyoptimistic-
1
@tmyklebu Дело в том, что OP намеревался сравнить скорость приращений, декрементов и сравнений между целыми и длинными. Вместо этого (при условии, что этот ответ правильный) они измеряли только сравнения и только против 0, что является частным случаем. По крайней мере, это вводит в заблуждение исходный тест - похоже, он измеряет три общих случая, тогда как на самом деле он измеряет один конкретный случай.
yshavit 07
1
@tmyklebu Не поймите меня неправильно, я проголосовал за вопрос, этот ответ и ваш ответ. Но я не согласен с вашим утверждением, что @chrylis корректирует эталонный тест, чтобы перестать измерять разницу, которую он пытается измерить. OP может исправить меня, если я ошибаюсь, но не похоже, что они пытаются только / в первую очередь измерять == 0, что, по-видимому, является непропорционально большой частью результатов тестов. Мне кажется более вероятным, что OP пытается измерить более общий диапазон операций, и этот ответ указывает на то, что эталонный тест сильно смещен в сторону только одной из этих операций.
yshavit 07
2
@tmyklebu Вовсе нет. Я полностью за понимание коренных причин. Но, определив, что одна из основных причин заключается в том, что эталонный тест был перекос, не является недопустимым изменить тест, чтобы удалить перекос, а также углубиться и понять больше об этом перекосе (например, что он может обеспечить более эффективный байт-код, чтобы упростить развертывание циклов и т. д.). Вот почему я поддержал как этот ответ (который выявил перекос), так и ваш (который более подробно рассматривает перекос).
yshavit 07
8

Базовая единица данных в виртуальной машине Java - слово. Выбор правильного размера слова остается после реализации JVM. Реализация JVM должна выбирать минимальный размер слова 32 бита. Он может выбрать больший размер слова для повышения эффективности. Также нет никаких ограничений, что 64-битная JVM должна выбирать только 64-битное слово.

Базовая архитектура не требует, чтобы размер слова был одинаковым. JVM читает / записывает данные пословно. Это причина, по которой это может занять больше времени, чем int .

Здесь вы можете найти больше по той же теме.

Вайбхав Радж
источник
4

Я только что написал тест с помощью штангенциркуля .

Эти результаты вполне согласуются с исходным кодом: а ~ 12x ускорение для использования intболее long. Кажется, конечно, что происходит разворачивание цикла, о котором сообщает tmyklebu или что-то очень похожее.

timeIntDecrements         195,266,845.000
timeLongDecrements      2,321,447,978.000

Это мой код; обратите внимание, что он использует только что созданный снимок caliper, поскольку я не мог понять, как кодировать их существующую бета-версию.

package test;

import com.google.caliper.Benchmark;
import com.google.caliper.Param;

public final class App {

    @Param({""+1}) int number;

    private static class IntTest {
        public static int v;
        public static void reset() {
            v = Integer.MAX_VALUE;
        }
        public static boolean decrementAndCheck() {
            return --v < 0;
        }
    }

    private static class LongTest {
        public static long v;
        public static void reset() {
            v = Integer.MAX_VALUE;
        }
        public static boolean decrementAndCheck() {
            return --v < 0;
        }
    }

    @Benchmark
    int timeLongDecrements(int reps) {
        int k=0;
        for (int i=0; i<reps; i++) {
            LongTest.reset();
            while (!LongTest.decrementAndCheck()) { k++; }
        }
        return (int)LongTest.v | k;
    }    

    @Benchmark
    int timeIntDecrements(int reps) {
        int k=0;
        for (int i=0; i<reps; i++) {
            IntTest.reset();
            while (!IntTest.decrementAndCheck()) { k++; }
        }
        return IntTest.v | k;
    }
}
Tucuxi
источник
1

Для справки, эта версия делает грубую "разминку":

public class LongSpeed {

    private static long i = Integer.MAX_VALUE;
    private static int j = Integer.MAX_VALUE;

    public static void main(String[] args) {

        for (int x = 0; x < 10; x++) {
            runLong();
            runWord();
        }
    }

    private static void runLong() {
        System.out.println("Starting the long loop");
        i = Integer.MAX_VALUE;
        long startTime = System.currentTimeMillis();
        while(!decrementAndCheckI()){

        }
        long endTime = System.currentTimeMillis();

        System.out.println("Finished the long loop in " + (endTime - startTime) + "ms");
    }

    private static void runWord() {
        System.out.println("Starting the word loop");
        j = Integer.MAX_VALUE;
        long startTime = System.currentTimeMillis();
        while(!decrementAndCheckJ()){

        }
        long endTime = System.currentTimeMillis();

        System.out.println("Finished the word loop in " + (endTime - startTime) + "ms");
    }

    private static boolean decrementAndCheckI() {
        return --i < 0;
    }

    private static boolean decrementAndCheckJ() {
        return --j < 0;
    }

}

Общее время улучшается примерно на 30%, но соотношение между ними остается примерно таким же.

Горячие лижет
источник
@TedHopp - Я попытался изменить пределы цикла в моем, и он остался практически неизменным.
Hot Licks
@ Techrocket9: я получаю аналогичные числа ( intв 20 раз быстрее) с этим кодом.
tmyklebu 07
1

Для записей:

если я использую

boolean decrementAndCheckLong() {
    lo = lo - 1l;
    return lo < -1l;
}

(изменено "l--" на "l = l - 1l") длительная работа улучшается на ~ 50%

Р. Мюллер
источник
0

У меня нет 64-битной машины для тестирования, но довольно большая разница говорит о том, что работает не только чуть более длинный байт-код.

Я вижу очень близкие времена для long / int (4400 против 4800 мс) на моем 32-битном 1.7.0_45.

Это только предположение , но я сильно подозреваю, что это результат штрафа за рассогласование памяти. Чтобы подтвердить / опровергнуть подозрение, попробуйте добавить публичный статический int dummy = 0; до объявления i. Это подтолкнет i вниз на 4 байта в макете памяти и может правильно выровнять его для повышения производительности. Подтверждено, что проблема не возникает.

РЕДАКТИРОВАТЬ: Причина этого в том, что виртуальная машина не может переупорядочивать поля на досуге, добавляя заполнение для оптимального выравнивания, поскольку это может мешать JNI. (Не тот случай).

Дюрандаль
источник
ВМ конечно , разрешено изменять порядок полей и добавлять заполнение.
Hot Licks
JNI должен получать доступ к объектам через эти надоедливые, медленные методы доступа, которые в любом случае требуют нескольких непрозрачных дескрипторов, поскольку сборщик мусора может происходить во время работы собственного кода. Вы можете свободно менять порядок полей и добавлять отступы.
tmyklebu 07