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

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

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

мое единственное предположение заключается в том, что CPU занимает больше времени, чтобы добавить 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;
    }

}

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

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

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

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



}

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

7 ответов


мой 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    xfffffffffffffffe,%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    xfffffffffffffffe,%r9d
0x00007f3dc290b5d5: mov    %r9d,%r8d
0x00007f3dc290b5d8: neg    %r8d
0x00007f3dc290b5db: sar    x1f,%r8d
0x00007f3dc290b5df: shr    x1f,%r8d
0x00007f3dc290b5e3: sub    %r9d,%r8d
0x00007f3dc290b5e6: sar    %r8d
0x00007f3dc290b5e9: neg    %r8d
0x00007f3dc290b5ec: and    xfffffffffffffffe,%r8d
0x00007f3dc290b5f0: shl    %r8d
0x00007f3dc290b5f3: mov    %r8d,%r11d
0x00007f3dc290b5f6: neg    %r11d
0x00007f3dc290b5f9: sar    x1f,%r11d
0x00007f3dc290b5fd: shr    x1e,%r11d
0x00007f3dc290b601: sub    %r8d,%r11d
0x00007f3dc290b604: sar    x2,%r11d
0x00007f3dc290b608: neg    %r11d
0x00007f3dc290b60b: and    xfffffffffffffffe,%r11d
0x00007f3dc290b60f: shl    x2,%r11d
0x00007f3dc290b613: mov    %r11d,%r9d
0x00007f3dc290b616: neg    %r9d
0x00007f3dc290b619: sar    x1f,%r9d
0x00007f3dc290b61d: shr    x1d,%r9d
0x00007f3dc290b621: sub    %r11d,%r9d
0x00007f3dc290b624: sar    x3,%r9d
0x00007f3dc290b628: neg    %r9d
0x00007f3dc290b62b: and    xfffffffffffffffe,%r9d
0x00007f3dc290b62f: shl    x3,%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    xfffffffffffffff0,%ecx
0x00007f3dc290b643: mov    %ecx,0x258(%r10)
0x00007f3dc290b64a: cmp    %r11d,%ecx
0x00007f3dc290b64d: jg     0x00007f3dc290b640

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

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

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


стек 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 загрузит значение вашего static i (0), вычесть один (3-4), дублировать значение в стеке (5) и вернуть его в переменную (6). Затем он выполняет ветвь compare-with-zero и возвращает.

версия с 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. Если это так, вы увидите очень мало разницы в производительности после прогрева.

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


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

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

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


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

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

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

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


для записи:

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

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

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


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

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

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

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