Большая разница в скорости эквивалентных статических и нестатических методов

86

В этом коде, когда я создаю объект в mainметоде, а затем вызываю этот метод объектов: ff.twentyDivCount(i)(выполняется за 16010 мс), он выполняется намного быстрее, чем его вызов с использованием этой аннотации: twentyDivCount(i)(выполняется за 59516 мс). Конечно, когда я запускаю его, не создавая объекта, я делаю метод статическим, чтобы его можно было вызывать в main.

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. * Исходный результат, кажется, постоянно воспроизводится.

Stabbz
источник
4
Как вы проводите свой тест? Готов поспорить, что это артефакт JVM, у которого не хватает времени на оптимизацию кода.
Патрик Коллинз
2
Кажется, JVM достаточно времени для компиляции и выполнения OSR для основного метода, как +PrintCompilation +PrintInliningпоказано
Тагир Валеев
1
Я пробовал фрагмент кода, но я не заметил такой разницы во времени, как сказал Стаббз. Они 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 восстанавливается исходное поведение (по крайней мере, на моей машине, мой ответ). Похоже, что статическая версия была на 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

Для желающих поэкспериментировать может пригодиться моя версия .

Maaartinus
источник
Это время «1456 мс»? Если да, то почему вы говорите, что статика медленная?
Тони
@Tony я смущен NON_STATICи STATIC, но мой вывод был прав. Исправлено, спасибо.
maaartinus
0

Когда это выполняется в режиме отладки, номера одинаковы для экземпляра и статического случая. Это также означает, что JIT не решается скомпилировать код в машинный код в статическом случае так же, как и в случае метода экземпляра.

Почему это так? Трудно сказать; вероятно, было бы правильно, если бы это было приложение большего размера ...

Драган Божанович
источник
«Почему это происходит? Трудно сказать, возможно, это было бы правильно, если бы это было более крупное приложение». Или у вас просто возникнет странная проблема с производительностью, которую невозможно отладить. (И это не так сложно сказать. Вы можете посмотреть на сборку, которую JVM выплевывает, как assylias.)
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 извините, я не добавил ссылку, о которой думал :). 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;
    }
}
Chengpohi
источник
От 20273 мс до 23000+ мс, разные для каждого прогона
Stabbz