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

В этом коде, когда я создаю объект в методе 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");
    }
}

EDIT: Пока кажется, что разные машины производят разные результаты, но с использованием JRE 1.8. *, где исходный результат, кажется, постоянно воспроизводится.

Ответ 1

Используя JRE 1.8.0_45, я получаю аналогичные результаты.

Исследование:

  • запуск java с параметрами -XX:+UnlockDiagnosticVMOptions -XX:+PrintCompilation -XX:+PrintInlining VM показывает, что оба метода скомпилированы и встроены
  • Глядя на сгенерированную сборку для самих методов, нет существенной разницы.
  • Однако, как только они встают в очередь, сгенерированная сборка внутри main сильно отличается, причем метод экземпляра более агрессивно оптимизируется, особенно в плане разворачивания цикла

Затем я снова проверил ваш тест, но с различными настройками разворота цикла, чтобы подтвердить подозрение выше. Я запустил ваш код с помощью:

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

Как вывод, кажется, что с настройками по умолчанию JIT hotspot 1.8.0_45 не может развернуть цикл, когда метод статичен (хотя я не уверен, почему он ведет себя таким образом). Другие JVM могут давать разные результаты.

Ответ 2

Только что недоказанное предположение основано на ответе assylias.

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

Для тех, кто хочет экспериментировать, моя версия может быть полезна.

Ответ 3

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

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

Ответ 4

Я немного изменил тест и получил следующие результаты:

Вывод:

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

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

Основываясь на этом тесте:

I предположим, что все зависит от оптимизации JVM. таким образом, я просто рекомендую вам перейти с эмпирическим правилом для использования статических и динамические методы.

ПРАВИЛО THUMB:

Java: когда использовать статические методы

Ответ 5

Попробуйте:

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