2013-11-07 3 views
84

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

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

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

Я запускаю это в 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 (ниже), той же системы. long: 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; 



} 

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

+8

Наиболее вероятный подозреваемый - это ваша настройка, а не центральный процессор или различные части JVM. Можете ли вы надежно воспроизвести это измерение? Не повторяя цикл, не разогревая JIT, используя 'currentTimeMillis()', запускающий код, который можно тривиально полностью оптимизировать и т. Д., Наносит ненадежный результат. – delnan

+1

Я сравнивал некоторое время назад, мне пришлось использовать 'long' в качестве счетчика циклов, потому что компилятор JIT оптимизировал цикл, когда я использовал' int'. Нужно будет посмотреть на разборку сгенерированного машинного кода. – Sam

+1

@ ZongZhengLi Да, результат вполне повторяемый. – Techrocket9

ответ

74

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 с; первых, есть некоторые screwiness, что я не претендую, чтобы понять, но выглядит как установки для развернутого цикла:

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

+0

Я действительно удивлен, что JIT не может устранить все вызовы 'decmentAndCheck()' как мертвого кода, так как у них нет побочных эффектов. –

+0

@AndrewBissell: Он может просто превратить 'doit' в тайм-код вокруг' i = 0'. Это не так. Он превращает его в разные вещи способами, которые, по-видимому, зависят от типа данных. – tmyklebu

+0

@tmyklebu Какой JVM? – chrylis

7

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

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

Here Вы можете найти более подробную информацию по одной теме.

21

Стек JVM определяется в терминах слов, размер которых является детальностью реализации, но должен быть шириной не менее 32 бит. Разработчик JVM может использовать 64-битные слова, но байт-код не может полагаться на это, и поэтому операции с long или double значениями должны обрабатываться с особой тщательностью. В частности, the JVM integer branch instructions определены точно так же, как и тип 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  

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

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

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

Я рекомендую вам write a correct microbenchmark устранить эффект наличия ЛТ пинок, а также попробовать это с окончательным условием, что не равен нулю, чтобы заставить JVM делать то же самое сравнение на int, что он делает с long.

+0

Не должно ли это повлиять на 32-битные JVM? Другие комментарии и ответ показывают, что 32-разрядная JVM не показывает этот симптом. – Katona

+0

«[Принуждение] JVM делает то же сравнение в int, что и с длинным», похоже, корректирует ваш тест, чтобы не измерить разницу, которую он пытается измерить. – tmyklebu

+1

@ Катона Не обязательно. В частности, JVM клиента и сервера HotSpot - это совершенно разные реализации, и Илья не указал на выбор сервера (клиент обычно является 32-битным значением по умолчанию). – chrylis

0

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

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

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

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

+0

нет разницы ниже 1.7.0_25 64-разрядная – tucuxi

+0

VM конечно * * разрешено изменять порядок полей и добавлять дополнения. –

+0

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

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

+0

@TedHopp - Я попытался изменить пределы цикла в шахте и остался практически без изменений. –

+0

@ Techrocket9: Я получаю аналогичные числа ('int' в 20 раз быстрее) с помощью этого кода. – tmyklebu

1

Для записей:

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

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

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

4

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

results вполне соответствуют исходному коду: ускорение ~ 12x для использования int над long. Конечно, кажется, что цикл разворачивается reported by 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; 
    } 
} 
Смежные вопросы