Duża różnica prędkości równoważnych metod statycznych i niestatycznych

86

W tym kodzie, kiedy tworzę obiekt w mainmetodzie, a następnie wywołuję metodę obiektów: ff.twentyDivCount(i)(działa w 16010 ms), działa znacznie szybciej niż wywołanie go za pomocą tej adnotacji: twentyDivCount(i)(działa w 59516 ms). Oczywiście, gdy uruchamiam go bez tworzenia obiektu, ustawiam metodę jako statyczną, aby można ją było wywołać w pliku 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");
    }
}

EDYCJA: Jak dotąd wydaje się, że różne maszyny dają różne wyniki, ale użycie JRE 1.8. * Jest tam, gdzie oryginalny wynik wydaje się być konsekwentnie odtwarzany.

Stabbz
źródło
4
Jak oceniasz swój test porównawczy? Założę się, że jest to artefakt JVM, który nie ma wystarczająco dużo czasu, aby zoptymalizować kod.
Patrick Collins
2
Wygląda na to, że JVM ma wystarczająco dużo czasu, aby skompilować i wykonać OSR dla głównej metody, jak +PrintCompilation +PrintInliningpokazano
Tagir Valeev
1
Wypróbowałem fragment kodu, ale nie widzę takiej różnicy czasu, jak powiedział Stabbz. 56282ms (przy użyciu instancji) 54551ms (jako metoda statyczna).
Don Chakkappan
1
@PatrickCollins Musi wystarczyć pięć sekund. I przepisał to trochę tak, że można mierzyć zarówno (jeden JVM rozkręci za wariantu). Wiem, że jako test porównawczy jest nadal wadliwy, ale jest wystarczająco przekonujący: 1457 ms STATIC vs 5312 ms NON_STATIC.
maaartinus
1
Nie zbadałem jeszcze szczegółowo tej kwestii, ale może to być powiązane: shipilev.net/blog/2015/black-magic-method-dispatch (może Aleksey Shipilëv może nas tutaj oświecić)
Marco13

Odpowiedzi:

72

Używając JRE 1.8.0_45 uzyskuję podobne wyniki.

Dochodzenie:

  1. uruchomienie java z -XX:+UnlockDiagnosticVMOptions -XX:+PrintCompilation -XX:+PrintInliningopcjami VM pokazuje, że obie metody są kompilowane i wbudowywane
  2. Spojrzenie na wygenerowany zestaw pod kątem samych metod nie wykazuje znaczącej różnicy
  3. Jednak gdy zostaną wstawione, wygenerowany zespół mainjest bardzo różny, a metoda instancji jest bardziej agresywnie zoptymalizowana, szczególnie pod względem rozwijania pętli

Następnie ponownie przeprowadziłem test, ale z innymi ustawieniami rozwijania pętli, aby potwierdzić powyższe podejrzenie. Uruchomiłem twój kod z:

  • -XX:LoopUnrollLimit=0 a obie metody działają wolno (podobnie do metody statycznej z domyślnymi opcjami).
  • -XX:LoopUnrollLimit=100 a obie metody działają szybko (podobnie do metody instancji z domyślnymi opcjami).

Podsumowując, wydaje się, że przy domyślnych ustawieniach JIT hotspotu 1.8.0_45 nie jest w stanie rozwinąć pętli, gdy metoda jest statyczna (chociaż nie jestem pewien, dlaczego zachowuje się w ten sposób). Inne maszyny JVM mogą dawać inne wyniki.

asylias
źródło
Między 52 a 71 przywracane jest oryginalne zachowanie (przynajmniej na moim komputerze, patrz moja odpowiedź). Wygląda na to, że wersja statyczna była o 20 jednostek większa, ale dlaczego? To jest dziwne.
maaartinus
3
@maaartinus Nie jestem nawet pewien, co dokładnie reprezentuje ta liczba - dokument jest dość wymijający: „ Rozwiń ciała pętli z pośrednią reprezentacją kompilatora serwera, licząc mniej niż ta wartość. Limit używany przez kompilator serwera jest funkcją tej wartości, a nie rzeczywistą wartością . Wartość domyślna różni się w zależności od platformy, na której działa maszyna JVM. "...
assylias
Ani nie wiem, ale moje pierwsze przypuszczenie było takie, że metody statyczne stają się nieco większe w dowolnych jednostkach i że trafiliśmy w miejsce, w którym ma to znaczenie. Jednak różnica jest dość duża, więc moje obecne przypuszczenie jest takie, że wersja statyczna ma pewne optymalizacje, które sprawiają, że jest nieco większa. Nie patrzyłem na wygenerowany asm.
maaartinus
33

Tylko nieudowodnione przypuszczenie oparte na odpowiedzi asyliów.

JVM używa progu do rozwijania pętli, który wynosi około 70. Z jakiegoś powodu statyczne wywołanie jest nieco większe i nie jest rozwijane.

Zaktualizuj wyniki

  • Z LoopUnrollLimitponiżej 52, obie wersje są wolne.
  • Między 52 a 71 tylko wersja statyczna jest wolna.
  • Powyżej 71 obie wersje są szybkie.

Jest to dziwne, ponieważ przypuszczałem, że statyczne wywołanie jest tylko nieco większe w wewnętrznej reprezentacji, a OP trafił w dziwny przypadek. Ale różnica wydaje się wynosić około 20, co nie ma sensu.

 

-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

Dla chętnych do eksperymentowania moja wersja może się przydać.

maaartinus
źródło
Czy to czas „1456 ms”? Jeśli tak, dlaczego mówisz, że statyczność jest wolna?
Tony
@Tony Myliłem się NON_STATICi STATIC, ale mój wniosek był słuszny. Naprawiono teraz, dziękuję.
maaartinus
0

Gdy jest to wykonywane w trybie debugowania, liczby są takie same dla instancji i przypadków statycznych. Oznacza to ponadto, że JIT waha się przed kompilacją kodu do kodu natywnego w przypadku statycznym w taki sam sposób, jak w przypadku metody instancji.

Dlaczego to robi? Trudno powiedzieć; prawdopodobnie zrobiłoby to dobrze, gdyby była to większa aplikacja ...

Dragan Bozanovic
źródło
„Dlaczego to robi? Trudno powiedzieć, prawdopodobnie dobrze by się stało, gdyby była to większa aplikacja”. Albo po prostu masz dziwny problem z wydajnością, który jest zbyt duży, aby faktycznie debugować. (I nie jest tak trudno powiedzieć. Możesz spojrzeć na zgromadzenie, które JVM wypluwa jak asylias.)
tmyklebu
@tmyklebu Lub mamy dziwny problem z wydajnością, którego pełne debugowanie jest niepotrzebne i kosztowne, i są łatwe obejścia. Na koniec mówimy tutaj o JIT, jego autorzy nie wiedzą, jak dokładnie zachowuje się we wszystkich sytuacjach. :) Spójrz na inne odpowiedzi, są one bardzo dobre i bardzo blisko wyjaśnienia problemu, ale jak dotąd nikt nie wie, dlaczego tak się dzieje.
Dragan Bozanovic
@DraganBozanovic: Przestaje być „niepotrzebne do pełnego debugowania”, gdy powoduje rzeczywiste problemy w rzeczywistym kodzie.
tmyklebu
0

Po prostu poprawiłem test i otrzymałem następujące wyniki:

Wynik:

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

UWAGA

Gdy testowałem je oddzielnie, uzyskałem ~ 52 sekundy na dynamiczne i ~ 200 sekund na statyczne.

Oto program:

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

Zmieniłem też kolejność testu na:

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

I mam to:

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

Jak widać, jeśli dynamika jest wywoływana przed statyczną, prędkość statyczna drastycznie spada.

Na podstawie tego testu porównawczego:

Mam hipotezę, że to wszystko zależy od optymalizacji JVM. w związku z tym po prostu zalecam stosowanie praktycznej zasady dotyczącej stosowania metod statycznych i dynamicznych.

PRAKTYCZNA ZASADA:

Java: kiedy używać metod statycznych

nafas
źródło
„kieruj się praktyczną zasadą stosowania metod statycznych i dynamicznych”. Jaka jest ta praktyczna zasada? A kto / co cytujesz?
weston
@weston przepraszam, nie dodałem linku, o którym myślałem :). thx
nafas
0

Proszę spróbować:

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
źródło
20273 ms do 23000+ ms, różne dla każdego przebiegu
Stabbz