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

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

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

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

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

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;
    }

}

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

В режиме 64-разрядной версии: long: 875 мс длинный: 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;



}

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

Ответ 1

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

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 */

Он обманывает, сильно, когда вы используете int s; сначала там какая-то болтливость, которую я не утверждаю, чтобы понять, но выглядит как установка для развернутого цикла:

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 раз быстрее для int, потому что 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, чтобы эта работа была для вас; вам нужно поместить некоторую случайную общую библиотеку в нужное место или она не удастся.

Ответ 2

Стек 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.

Ответ 3

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

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

Здесь вы можете найти более подробную информацию по этой же теме.

Ответ 4

Я только что написал тест, используя caliper.

результаты вполне соответствуют исходному коду: ускорение ~ 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;
    }
}

Ответ 5

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

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%, но соотношение между ними остается примерно одинаковым.

Ответ 6

Для записей:

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

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

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

Ответ 7

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

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

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

EDIT: Причиной этого является то, что виртуальная машина не может переупорядочивать поля в своем досуге, добавляя дополнение для оптимального выравнивания, поскольку это может помешать JNI (не в случае).