Program Java działa wolniej, gdy kod, który nie jest wykonywany, jest komentowany

Zaobserwowałem dziwne zachowanie w jednym z moich programów Java. Próbowałem usunąć kod w jak największym stopniu, jednocześnie będąc w stanie odtworzyć zachowanie. Kod w całości poniżej.

public class StrangeBehaviour {

    static boolean recursionFlag = true;

    public static void main(String[] args) {
        long startTime = System.nanoTime();
        for (int i = 0; i < 10000; i ++) {
            functionA(6, 0);
        }
        long endTime = System.nanoTime();
        System.out.format("%.2f seconds elapsed.\n", (endTime - startTime) / 1000.0 / 1000 / 1000);
    }

    static boolean functionA(int recursionDepth, int recursionSwitch) {
        if (recursionDepth == 0) { return true; }
        return functionB(recursionDepth, recursionSwitch);
    }

    static boolean functionB(int recursionDepth, int recursionSwitch) {
        for (int i = 0; i < 16; i++) {
            if (StrangeBehaviour.recursionFlag) {
                if (recursionSwitch == 0) {
                    if (functionA(recursionDepth - 1, 1 - recursionSwitch)) return true;
                } else {
                    if (!functionA(recursionDepth - 1, 1 - recursionSwitch)) return false;
                }
            } else {
                // This block is never entered into.
                // Yet commenting out one of the lines below makes the program run slower!
                System.out.println("...");
                System.out.println("...");
                System.out.println("...");
                System.out.println("...");
                System.out.println("...");
                System.out.println("...");
                System.out.println("...");
                System.out.println("...");
                System.out.println("...");
                System.out.println("...");
                System.out.println("...");
                System.out.println("...");
                System.out.println("...");
                System.out.println("...");
                System.out.println("...");
                System.out.println("...");
                System.out.println("...");
                System.out.println("...");
                System.out.println("...");
                System.out.println("...");
                System.out.println("...");
                System.out.println("...");
                System.out.println("...");
                System.out.println("...");
                System.out.println("...");
                System.out.println("...");
                System.out.println("...");
                System.out.println("...");
                System.out.println("...");
                System.out.println("...");
                System.out.println("...");
                System.out.println("...");
                System.out.println("...");
                System.out.println("...");
            }
        }
        return false;
    }
}

Mam dwie funkcje, functionA() i functionB(), które wywołują się rekurencyjnie. Obie funkcje przyjmują parametr recursionDepth kontrolujący zakończenie rekurencji. functionA() wywołuje functionB() maksymalnie raz z recursionDepth bez zmian. functionB() połączenia functionA() 16 razy z recursionDepth - 1. Rekurencja kończy się gdy functionA() jest wywoływana przez recursionDepth z 0.

functionB() posiada blok kodu z liczbą wywołań System.out.println(). Blok ten nigdy nie jest wprowadzany, ponieważ wpis jest kontrolowany przez zmienną boolean recursionFlag ustawioną na true i nigdy nie zmienianą podczas wykonywania programu. Jednak komentowanie nawet jednego wywołania println() powoduje, że program działa wolniej. Na moim komputerze czas wykonania wynosi println() i > 2s, gdy jedno z wywołań jest komentowane.

Co może być spowodowanie takiego zachowania? Domyślam się tylko, że istnieje naiwna optymalizacja kompilatora, która jest wywoływana przez parametr związany z długością bloku kodu (lub liczbą wywołań funkcji itp.). Wszelkie dalsze informacje na ten temat będą mile widziane!

Edit: używam JDK 1.8.

Author: J3D1, 2016-12-31

3 answers

Pełna odpowiedź jest kombinacją odpowiedzi k5_ i Tony ' ego.

Kod opublikowany przez OP pomija pętlę rozgrzewki, aby uruchomić kompilację hotspota przed wykonaniem testu; stąd 10-krotne (na moim komputerze) przyspieszenie, gdy instrukcje print są włączone, łączy zarówno czas spędzony w hotspocie na skompilowanie kodu bajtowego do instrukcji CPU, jak również rzeczywiste działanie instrukcji CPU.

Jeśli dodam oddzielną pętlę rozgrzewającą przed pętlą czasową, jest tylko 2,5-krotne przyspieszenie z instrukcją print.

To wskazuje, że zarówno Kompilacja HotSpot/JIT trwa dłużej, gdy metoda jest inlined (jak wyjaśnił Tony), jak również, że uruchamianie kodu trwa dłużej, prawdopodobnie z powodu gorszej wydajności cache lub branch-prediction/pipelining, jak pokazał k5_.

public static void main(String[] args) {
    // Added the following warmup loop before the timing loop
    for (int i = 0; i < 50000; i++) {
        functionA(6, 0);
    }

    long startTime = System.nanoTime();
    for (int i = 0; i < 50000; i++) {
        functionA(6, 0);
    }
    long endTime = System.nanoTime();
    System.out.format("%.2f seconds elapsed.\n", (endTime - startTime) / 1000.0 / 1000 / 1000);
}
 21
Author: Erwin Bolwidt,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-01-02 06:09:25

Komentowany kod wpływa na sposób obsługi inliningu. Jeśli functionB będzie dłuższy/większy (więcej instrukcji kodu bajtowego), to nie będzie on inlinowany do functionA.

Więc @J3D1 był w stanie użyć VMOptions, aby ręcznie wyłączyć inlining dla functionB(): -XX:CompileCommand=dontinline,com.jd.benchmarking.StrangeBeh‌​aviour::functionB wydaje się to eliminować opóźnienie z krótszą funkcją.

Z opcjami vm można wyświetlić inlining -XX:+UnlockDiagnosticVMOptions -XX:+PrintInlining

Większa wersja, wont inline functionB

@ 8   StrangeBehaviour::functionB (326 bytes)   callee is too large
@ 21   StrangeBehaviour::functionA (12 bytes)
  @ 8   StrangeBehaviour::functionB (326 bytes)   callee is too large
@ 35   StrangeBehaviour::functionA (12 bytes)
  @ 8   StrangeBehaviour::functionB (326 bytes)   callee is too large

Krótsza wersja będzie spróbuj inline functionB, powodując kilka dalszych prób.

@ 8   StrangeBehaviour::functionB (318 bytes)   inline (hot)
 @ 21   StrangeBehaviour::functionA (12 bytes)   inline (hot)
   @ 8   StrangeBehaviour::functionB (318 bytes)   inline (hot)
     @ 35   StrangeBehaviour::functionA (12 bytes)   recursive inlining is too deep
 @ 35   StrangeBehaviour::functionA (12 bytes)   inline (hot)
   @ 8   StrangeBehaviour::functionB (318 bytes)   inline (hot)
     @ 21   StrangeBehaviour::functionA (12 bytes)   recursive inlining is too deep
     @ 35   StrangeBehaviour::functionA (12 bytes)   recursive inlining is too deep
@ 21   StrangeBehaviour::functionA (12 bytes)   inline (hot)
 @ 8   StrangeBehaviour::functionB (318 bytes)   inline (hot)
   @ 35   StrangeBehaviour::functionA (12 bytes)   inline (hot)
     @ 8   StrangeBehaviour::functionB (318 bytes)   recursive inlining is too deep
@ 35   StrangeBehaviour::functionA (12 bytes)   inline (hot)
 @ 8   StrangeBehaviour::functionB (318 bytes)   inline (hot)
   @ 21   StrangeBehaviour::functionA (12 bytes)   inline (hot)
    @ 8   StrangeBehaviour::functionB (318 bytes)   recursive inlining is too deep
   @ 35   StrangeBehaviour::functionA (12 bytes)   inline (hot)
     @ 8   StrangeBehaviour::functionB (318 bytes)   recursive inlining is too deep

Głównie zgadywanie, ale większy / inlined bytecode spowoduje problemy z przewidywaniem gałęzi i buforowaniem

 41
Author: k5_,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-01-05 04:35:30

Jestem z @k5_, wydaje się, że istnieje próg, aby określić, czy do inline funkcji. Jeśli kompilator JIT zdecyduje się na jego wbudowanie, spowoduje to wiele pracy i czasu, aby zrobić to tak, jak pokazuje -XX:+PrintCompilation:

  task-id
    158   32       3       so_test.StrangeBehaviour::functionB (326 bytes)   made not entrant
    159   35       3       java.lang.String::<init> (82 bytes)
    160   36  s    1       java.util.Vector::size (5 bytes)
    1878   37 %     3       so_test.StrangeBehaviour::main @ 6 (65 bytes)
    1898   38       3       so_test.StrangeBehaviour::main (65 bytes)
    2665   39       3       java.util.regex.Pattern::has (15 bytes)
    2667   40       3       sun.misc.FDBigInteger::mult (64 bytes)
    2668   41       3       sun.misc.FDBigInteger::<init> (30 bytes)
    2668   42       3       sun.misc.FDBigInteger::trimLeadingZeros (57 bytes)
    2.51 seconds elapsed.

Górny jest info Bez komentarza, poniżej znajduje się komentarz, który zmniejsza rozmiar metody z 326 bajtów do 318 bajtów. I można zauważyć, że identyfikator zadania w kolumnie 1 wyjścia jest znacznie większy w tym ostatnim, co powoduje więcej czasu.

  task-id
    126   35       4       so_test.StrangeBehaviour::functionA (12 bytes)
    130   33       3       so_test.StrangeBehaviour::functionA (12 bytes)   made not entrant
    131   36  s    1       java.util.Vector::size (5 bytes)
    14078   37 %     3       so_test.StrangeBehaviour::main @ 6 (65 bytes)
    14296   38       3       so_test.StrangeBehaviour::main (65 bytes)
    14296   39 %     4       so_test.StrangeBehaviour::functionB @ 2 (318 bytes)
    14300   40       4       so_test.StrangeBehaviour::functionB (318 bytes)
    14304   34       3       so_test.StrangeBehaviour::functionB (318 bytes)   made not entrant
    14628   41       3       java.util.regex.Pattern::has (15 bytes)
    14631   42       3       sun.misc.FDBigInteger::mult (64 bytes)
    14632   43       3       sun.misc.FDBigInteger::<init> (30 bytes)
    14632   44       3       sun.misc.FDBigInteger::trimLeadingZeros (57 bytes)
    14.50 seconds elapsed.

I jeśli zmieniasz kod na następujący (dodaj dwie linie i commnet z linii wydruku), widać, że rozmiar kodu zmienia się na 326 bajtów i działa szybciej:

        if (StrangeBehaviour.recursionFlag) {
            int a = 1;
            int b = 1;
            if (recursionSwitch == 0) {
                if (functionA(recursionDepth - 1, 1 - recursionSwitch)) return true;
            } else {
                if (!functionA(recursionDepth - 1, 1 - recursionSwitch)) return false;
            }
        } else {
            // This block is never entered into.
            // Yet commenting out one of the lines below makes the program run slower!
            System.out.println("...");
            System.out.println("...");
            System.out.println("...");
          //System.out.println("...");
            System.out.println("...");
            System.out.println("...");
            System.out.println("...");
            System.out.println("...");
            System.out.println("...");
            System.out.println("...");
            System.out.println("...");
            System.out.println("...");
            System.out.println("...");
            System.out.println("...");
            System.out.println("...");
            System.out.println("...");
            System.out.println("...");
            System.out.println("...");
            System.out.println("...");
            System.out.println("...");
            System.out.println("...");
            System.out.println("...");
            System.out.println("...");
            System.out.println("...");
            System.out.println("...");
            System.out.println("...");
            System.out.println("...");
            System.out.println("...");
            System.out.println("...");
            System.out.println("...");
            System.out.println("...");
            System.out.println("...");
            System.out.println("...");
            System.out.println("...");
        }

New time and JIT compiler info:

    140   34       3       so_test.StrangeBehaviour::functionB (326 bytes)   made not entrant
    145   36       3       java.lang.String::<init> (82 bytes)
    148   37  s    1       java.util.Vector::size (5 bytes)
    162   38       4       so_test.StrangeBehaviour::functionA (12 bytes)
    163   33       3       so_test.StrangeBehaviour::functionA (12 bytes)   made not entrant
    1916   39 %     3       so_test.StrangeBehaviour::main @ 6 (65 bytes)
    1936   40       3       so_test.StrangeBehaviour::main (65 bytes)
    2686   41       3       java.util.regex.Pattern::has (15 bytes)
    2689   42       3       sun.misc.FDBigInteger::mult (64 bytes)
    2690   43       3       sun.misc.FDBigInteger::<init> (30 bytes)
    2690   44       3       sun.misc.FDBigInteger::trimLeadingZeros (57 bytes)
    2.55 seconds elapsed.

Podsumowując :

    Gdy rozmiar metody przekracza pewne limity, JIT nie będzie wbudowywał tej funkcji; W przypadku, gdy mamy do czynienia z linijką, która zmniejsza się do wielkości poniżej progu, JIT decyduje się ją wstawić;]}
  • Inlining that function causes a lot zadań JIT, które spowalnia program.

Update :

W mojej ostatniej próbie odpowiedź na to pytanie nie jest taka łatwa:

Jak pokazuje mój przykład kodu, normalna optymalizacja w linii będzie

  • przyspiesza program
  • i nie kosztuje dużo pracy kompilatora(w moim teście, to nawet mniej pracy, gdy dzieje się inline).

Ale w tym problemie kod powoduje dużo pracy JIT i spowalnia program, który to chyba jakiś żart. I nadal nie jest jasne, dlaczego powoduje to tak wiele pracy JIT.

 18
Author: Tony,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-01-05 01:49:33