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.
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);
}
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.StrangeBehaviour::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
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.
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