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