Велика різниця в швидкості еквівалентних статичних та нестатичних методів


86

У цьому коді, коли я створюю об'єкт у mainметоді, а потім викликаю метод "об'єкти": ff.twentyDivCount(i)(працює за 16010 мс), він працює набагато швидше, ніж викликати його за допомогою цієї анотації: twentyDivCount(i)(працює за 59516 мс). Звичайно, коли я запускаю його без створення об'єкта, я роблю метод статичним, тому його можна викликати в основному.

public class ProblemFive {

    // Counts the number of numbers that the entry is evenly divisible by, as max is 20
    int twentyDivCount(int a) {    // Change to static int.... when using it directly
        int count = 0;
        for (int i = 1; i<21; i++) {

            if (a % i == 0) {
                count++;
            }
        }
        return count;
    }

    public static void main(String[] args) {
        long startT = System.currentTimeMillis();;
        int start = 500000000;
        int result = start;

        ProblemFive ff = new ProblemFive();

        for (int i = start; i > 0; i--) {

            int temp = ff.twentyDivCount(i); // Faster way
                       // twentyDivCount(i) - slower

            if (temp == 20) {
                result = i;
                System.out.println(result);
            }
        }

        System.out.println(result);

        long end = System.currentTimeMillis();;
        System.out.println((end - startT) + " ms");
    }
}

РЕДАКТУВАТИ: Поки що здається, що різні машини дають різні результати, але використання JRE 1.8. * - це місце, де вихідний результат, здається, послідовно відтворюється.


4
Як ви керуєте своїм тестом? Б'юся об заклад, що це артефакт JVM, який не має достатньо часу для оптимізації коду.
Патрік Коллінз,

2
Здається, для JVM достатньо часу, щоб скомпілювати та виконати OSR для основного методу, як +PrintCompilation +PrintInliningпоказано
Тагір Валєєв

1
Я спробував фрагмент коду, але я не отримую такої різниці в часі, як сказав Stabbz. Вони 56282 мс (з використанням екземпляра) 54551 мс (як статичний метод).
Дон Чаккаппан

1
@PatrickCollins П’яти секунд має бути достатньо. Я трохи переписав, щоб ви могли виміряти обидва (по одному JVM запускається на варіант). Я знаю, що в якості еталону він все ще має недоліки, але досить переконливий: 1457 мс STATIC проти 5312 мс NON_STATIC.
maaartinus

1
Ще не досліджували питання детально, але це може бути пов’язано: shipilev.net/blog/2015/black-magic-method-dispatch (можливо, Олексій Шипільєв може нас просвітити тут)
Marco13,

Відповіді:


72

Використовуючи JRE 1.8.0_45, я отримую подібні результати.

Розслідування:

  1. запуск Java з параметрами -XX:+UnlockDiagnosticVMOptions -XX:+PrintCompilation -XX:+PrintInliningвіртуальної машини показує, що обидва методи компілюються та вбудовуються
  2. Перегляд сформованої збірки для самих методів не виявляє суттєвої різниці
  3. Однак, коли вони вбудовані, згенерована збірка всередині mainсильно відрізняється, при цьому метод екземпляра є більш агресивно оптимізованим, особливо з точки зору розгортання циклу

Потім я знову провів ваш тест, але з різними налаштуваннями розгортання циклу, щоб підтвердити підозру вище. Я запустив ваш код за допомогою:

  • -XX:LoopUnrollLimit=0 і обидва методи працюють повільно (подібно до статичного методу з параметрами за замовчуванням).
  • -XX:LoopUnrollLimit=100 і обидва методи працюють швидко (подібно до методу екземпляра з параметрами за замовчуванням).

Як висновок, здається, що за замовчуванням налаштування JIT точки доступу 1.8.0_45 не може розгорнути цикл, коли метод є статичним (хоча я не впевнений, чому він поводиться так). Інші JVM можуть дати різні результати.


Між 52 і 71, початкова поведінка відновлюється (принаймні на моїй машині, s. Моя відповідь). Схоже, статична версія була на 20 одиниць більшою, але чому? Це дивно.
maaartinus

3
@maaartinus Я навіть не впевнений, що саме це число представляє - документ досить ухильний: " Розгортання тіл циклу з проміжним вузлом представлення компілятора сервера менше, ніж це значення. Обмеження, яке використовується компілятором сервера, є функцією цього значення, не фактичне значення . Значення за замовчуванням залежить від платформи, на якій працює JVM. "...
assylias

Я також не знаю, але моє перше припущення було те, що статичні методи стають трохи більшими в будь-яких одиницях і що ми потрапляємо в те місце, де це важливо. Однак різниця є досить великою, тому моє поточне припущення полягає в тому, що статична версія отримує деякі оптимізації, які роблять її трохи більшою. Я не дивився на сформований asm.
maaartinus

33

Просто недоказана здогадка, заснована на відповіді асилія.

JVM використовує поріг для розгортання циклу, який становить приблизно 70. З якоїсь причини статичний виклик трохи більший і не розгортається.

Оновити результати

  • З наведеними LoopUnrollLimitнижче 52, обидві версії повільні.
  • Між 52 і 71 повільною є лише статична версія.
  • Понад 71 обидві версії швидкі.

Це дивно, оскільки я здогадувався, що статичний виклик лише трохи більший у внутрішньому поданні, і OP потрапив у дивний випадок. Але різниця, здається, становить близько 20, що не має сенсу.

 

-XX:LoopUnrollLimit=51
5400 ms NON_STATIC
5310 ms STATIC
-XX:LoopUnrollLimit=52
1456 ms NON_STATIC
5305 ms STATIC
-XX:LoopUnrollLimit=71
1459 ms NON_STATIC
5309 ms STATIC
-XX:LoopUnrollLimit=72
1457 ms NON_STATIC
1488 ms STATIC

Для тих, хто хоче експериментувати, моя версія може бути корисною.


Чи "1456 мс"? Якщо так, чому ви кажете, що статичний повільний?
Тоні

@Tony Я розгубив NON_STATICі STATIC, але мій висновок був правильним. Виправлено зараз, дякую.
maaartinus

0

Коли це виконується в режимі налагодження, номери однакові для екземпляра та статичних випадків. Це додатково означає, що JIT вагається компілювати код до власного коду у статичному випадку так само, як це робить у випадку методу екземпляра.

Чому це робиться так? Важко сказати; напевно, він вчинив би правильно, якби це була більша програма ...


"Чому це робиться? Важко сказати, мабуть, він би вчинив правильно, якби це була більша програма". Або у вас просто є дивна проблема продуктивності, яка занадто велика, щоб насправді налагоджувати. (І це не так важко сказати. Ви можете подивитися на збірку, яку випльовує JVM, як це робили асилії).
tmyklebu

@tmyklebu Або у нас є дивна проблема продуктивності, яка непотрібна і дорога для повного налагодження, і є прості обхідні шляхи. Зрештою, ми говоримо про JIT тут, його автори не знають, як він поводиться точно у всіх ситуаціях. :) Подивіться на інші відповіді, вони дуже хороші і дуже близькі для пояснення проблеми, але поки що поки що ніхто не знає, чому саме це відбувається.
Драган Бозанович

@DraganBozanovic: Це перестає бути "непотрібним для повного налагодження", коли це викликає реальні проблеми в реальному коді.
tmyklebu

0

Я просто трохи підправив тест і отримав такі результати:

Вихід:

Dynamic Test:
465585120
232792560
232792560
51350 ms
Static Test:
465585120
232792560
232792560
52062 ms

ПРИМІТКА

Поки я тестував їх окремо, я отримав ~ 52 с для динамічного та ~ 200 с для статичного.

Це програма:

public class ProblemFive {

    // Counts the number of numbers that the entry is evenly divisible by, as max is 20
    int twentyDivCount(int a) {  // Change to static int.... when using it directly
        int count = 0;
        for (int i = 1; i<21; i++) {

            if (a % i == 0) {
                count++;
            }
        }
        return count;
    }

    static int twentyDivCount2(int a) {
         int count = 0;
         for (int i = 1; i<21; i++) {

             if (a % i == 0) {
                 count++;
             }
         }
         return count;
    }

    public static void main(String[] args) {
        System.out.println("Dynamic Test: " );
        dynamicTest();
        System.out.println("Static Test: " );
        staticTest();
    }

    private static void staticTest() {
        long startT = System.currentTimeMillis();;
        int start = 500000000;
        int result = start;

        for (int i = start; i > 0; i--) {

            int temp = twentyDivCount2(i);

            if (temp == 20) {
                result = i;
                System.out.println(result);
            }
        }

        System.out.println(result);

        long end = System.currentTimeMillis();;
        System.out.println((end - startT) + " ms");
    }

    private static void dynamicTest() {
        long startT = System.currentTimeMillis();;
        int start = 500000000;
        int result = start;

        ProblemFive ff = new ProblemFive();

        for (int i = start; i > 0; i--) {

            int temp = ff.twentyDivCount(i); // Faster way

            if (temp == 20) {
                result = i;
                System.out.println(result);
            }
        }

        System.out.println(result);

        long end = System.currentTimeMillis();;
        System.out.println((end - startT) + " ms");
    }
}

Я також змінив порядок тесту на:

public static void main(String[] args) {
    System.out.println("Static Test: " );
    staticTest();
    System.out.println("Dynamic Test: " );
    dynamicTest();
}

І я отримав це:

Static Test:
465585120
232792560
232792560
188945 ms
Dynamic Test:
465585120
232792560
232792560
50106 ms

Як бачите, якщо динамічний викликається перед статичним, швидкість для статичного різко зменшується.

Виходячи з цього еталону:

Я припускаю, що все залежить від оптимізації JVM. таким чином, я просто рекомендую вам дотримуватися емпіричного правила для використання статичних та динамічних методів.

ПРАКТИЧНЕ ПРАВИЛО:

Java: коли використовувати статичні методи


"Ви повинні користуватися емпіричним правилом для використання статичних та динамічних методів." Що це за правило? І кого / з чого ти цитуєш?
weston

@weston вибачте, що я не додав посилання, про яке думав :). thx
nafas

0

Будь ласка, спробуй:

public class ProblemFive {
    public static ProblemFive PROBLEM_FIVE = new ProblemFive();

    public static void main(String[] args) {
        long startT = System.currentTimeMillis();
        int start = 500000000;
        int result = start;


        for (int i = start; i > 0; i--) {
            int temp = PROBLEM_FIVE.twentyDivCount(i); // faster way
            // twentyDivCount(i) - slower

            if (temp == 20) {
                result = i;
                System.out.println(result);
                System.out.println((System.currentTimeMillis() - startT) + " ms");
            }
        }

        System.out.println(result);

        long end = System.currentTimeMillis();
        System.out.println((end - startT) + " ms");
    }

    int twentyDivCount(int a) {  // change to static int.... when using it directly
        int count = 0;
        for (int i = 1; i < 21; i++) {

            if (a % i == 0) {
                count++;
            }
        }
        return count;
    }
}

20273 мс до 23000+ мс, різні для кожного запуску
Stabbz
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.