Чому в x64 Java набагато повільніше, ніж int?


90

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

Наведений нижче код займає 1688 мс, якщо тип i довгий, і 109 мс, коли i - це int. Чому long (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 (нижче), тієї ж системи. довгий: 72265ms int: 74656ms Ці результати знаходились у 32-бітному режимі налагодження.

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

Це свідчить про те, що результатом, який я спостерігав, є дивацтво оптимізації 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, без суттєвих змін.


8
Найімовірнішим підозрюваним є ваша установка, а не центральний процесор або різні частини JVM. Чи можете ви надійно відтворити це вимірювання? Не повторення циклу, не розігрівання JIT, використання currentTimeMillis(), запуск коду, який можна тривіально повністю оптимізувати і т. Д., Відчуває ненадійні результати.

1
Я тестував деякий час тому, мені довелося використовувати longяк лічильник циклу, оскільки компілятор JIT оптимізував цикл, коли я використовував int. Потрібно було б розглянути розбирання сформованого машинного коду.
Сем

7
Це не правильний мікробенчмарк, і я б не сподівався, що його результати якимось чином відображають реальність.
Луїс Вассерман,

7
Усі коментарі, які оскаржують ОП за те, що він не написав належного мікровизначення Java, невимовно ледачі. Це те, що дуже легко зрозуміти, якщо ви просто подивитесь і побачите, що JVM робить з кодом.
tmyklebu

2
@maaartinus: Прийнята практика є загальноприйнятою практикою, оскільки вона працює навколо переліку відомих підводних каменів. У випадку з належними тестами Java ви хочете переконатися, що вимірюєте правильно оптимізований код, а не заміну в стеку, і ви хочете, щоб ваші вимірювання були чистими в кінці. OP виявив зовсім інше питання, і тестовий показник, який він надав, адекватно продемонстрував його. І, як зазначалося, перетворення цього коду на належний бенчмарк Java насправді не змушує дивацтва зникати. І читати код збірки не складно.
tmyklebu

Відповіді:


80

Мій 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

Тож для ints це відбувається в 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, щоб ця робота також працювала для вас; вам потрібно розмістити якусь випадкову спільну бібліотеку в точно потрібному місці, інакше вона не вдасться.


8
Добре, отже, net-net не те, що longверсія повільніша, а швидше, ніж intверсія швидша. Що має сенс. Ймовірно, не так багато зусиль було вкладено в те, щоб JIT оптимізував longвирази.
Hot Licks

1
... вибачте за мою необізнаність, але що таке "funroll"? Здається, я навіть не можу правильно пошукати цей термін, і це робить це перший раз, коли мені доводиться запитувати когось, що означає слово в Інтернеті.
BrianH

1
@BrianDHall gccвикористовує -fяк перемикач командного рядка для "прапор", і unroll-loopsоптимізація вмикається за допомогою вимови -funroll-loops. Я просто використовую "unroll" для опису оптимізації.
chrisis

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 не має вказівки порівнювати а longз нулем, тому йому доводиться натискати константу 0Lна стек (9), робити загальне longпорівняння (10), а потім розгалужувати значення цього розрахунку.

Ось два ймовірні сценарії:

  • JVM точно дотримується шляху байт-коду. У цьому випадку він робить більше роботи у longверсії, натискаючи та видаючи кілька додаткових значень, і вони знаходяться у віртуальному керованому стеку , а не в реальному апаратному стеку ЦП. У цьому випадку ви все одно побачите значну різницю в продуктивності після розминки.
  • JVM усвідомлює, що може оптимізувати цей код. У цьому випадку потрібен додатковий час, щоб оптимізувати частину практично непотрібної логіки натискання / порівняння. У цьому випадку ви побачите дуже незначну різницю в продуктивності після розминки.

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


1
@Katona Не обов'язково. Особливо, JVM клієнта та сервера HotSpot - це абсолютно різні реалізації, і Ілля не вказав вибір сервера (клієнт, як правило, є 32-розрядним за замовчуванням).
chrisis

1
@tmyklebu Питання в тому, що еталон вимірює відразу кілька різних речей. Використання ненульового термінального стану зменшує кількість змінних.
chrisis

1
@tmyklebu Справа в тому, що OP планував порівняти швидкість приростів, зменшень та порівнянь на ints проти longs. Натомість (якщо припустити, що ця відповідь правильна) вони вимірювали лише порівняння і лише проти 0, що є особливим випадком. Якщо нічого іншого, це робить оригінальний показник оманливим - схоже, він вимірює три загальні випадки, а насправді вимірює один, конкретний випадок.
yshavit

1
@tmyklebu Не сприймайте мене неправильно, я підтримав питання, цю відповідь та вашу відповідь. Але я не погоджуюсь з вашим твердженням, що @chrylis коригує еталон, щоб припинити вимірювати різницю, яку намагається виміряти. OP може виправити мене, якщо я помиляюся, але, схоже, вони намагаються лише / в основному виміряти == 0, що, здається, є непропорційно великою частиною результатів тесту. Мені здається більш імовірним, що ОП намагається виміряти більш загальний діапазон операцій, і ця відповідь вказує на те, що базовий показник дуже перекошений лише до однієї з цих операцій.
yshavit

2
@tmyklebu Зовсім не. Я все за розуміння першопричин. Але, виявивши, що однією з основних причин є те, що контрольний показник був перекошений, не є недійсним змінити контрольний показник, щоб усунути перекіс, а також скопати та зрозуміти більше про цей перекіс (наприклад, що це може забезпечити більш ефективну роботу байт-код, що полегшує розгортання циклів тощо). Ось чому я підтримав і цю відповідь (яка визначила перекіс), і вашу (яка копається в перекосі більш детально).
yshavit

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

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: Я отримую подібні цифри (у int20 разів швидше) з цим кодом.
tmyklebu

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 повинен отримувати доступ до об'єктів за допомогою цих надокучливих, повільних методів доступу, які в будь-якому випадку займають кілька непрозорих ручок, оскільки GC може відбуватися під час роботи власного коду. Досить безкоштовно змінювати порядок полів та додавати відступи.
tmyklebu
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.