Šādas Java programmas izpilde vidēji aizņem no 0,50 līdz 0,55 sekundēm:
public static void main(String[] args) {
long startTime = System.nanoTime();
int n = 0;
for (int i = 0; i < 1000000000; i++) {
n += 2 * (i * i);
}
System.out.println((double) (System.nanoTime() - startTime) / 1000000000 + " s");
System.out.println("n = " + n);
}
Ja 2 * (i * i)
aizstāj ar 2 * i * i
, tās izpilde ilgst no 0,60 līdz 0,65 sekundēm. Kā tas notiek?
Katru programmas versiju es palaidīju 15 reizes, mainot abas versijas. Šeit ir rezultāti:
2*(i*i) | 2*i*i
----------+----------
0.5183738 | 0.6246434
0.5298337 | 0.6049722
0.5308647 | 0.6603363
0.5133458 | 0.6243328
0.5003011 | 0.6541802
0.5366181 | 0.6312638
0.515149 | 0.6241105
0.5237389 | 0.627815
0.5249942 | 0.6114252
0.5641624 | 0.6781033
0.538412 | 0.6393969
0.5466744 | 0.6608845
0.531159 | 0.6201077
0.5048032 | 0.6511559
0.5232789 | 0.6544526
Ātrākais 2 * i * i
izpildījums ilga ilgāk nekā lēnākais 2 * (i * i)
izpildījums. Ja abas darbības būtu tikpat efektīvas, varbūtība, ka tas notiks, būtu mazāka par 1/2^15 * 100% = 0,00305%.
Bajtu kodi: https://cs.nyu.edu/courses/fall00/V22.0201-001/jvm2.html Bajtu kodu pārlūks: https://github.com/Konloch/bytecode-viewer
Manā JDK (Windows 10 64 bit, 1.8.0_65-b17) es varu reproducēt un izskaidrot:
public static void main(String[] args) {
int repeat = 10;
long A = 0;
long B = 0;
for (int i = 0; i < repeat; i++) {
A += test();
B += testB();
}
System.out.println(A / repeat + " ms");
System.out.println(B / repeat + " ms");
}
private static long test() {
int n = 0;
for (int i = 0; i < 1000; i++) {
n += multi(i);
}
long startTime = System.currentTimeMillis();
for (int i = 0; i < 1000000000; i++) {
n += multi(i);
}
long ms = (System.currentTimeMillis() - startTime);
System.out.println(ms + " ms A " + n);
return ms;
}
private static long testB() {
int n = 0;
for (int i = 0; i < 1000; i++) {
n += multiB(i);
}
long startTime = System.currentTimeMillis();
for (int i = 0; i < 1000000000; i++) {
n += multiB(i);
}
long ms = (System.currentTimeMillis() - startTime);
System.out.println(ms + " ms B " + n);
return ms;
}
private static int multiB(int i) {
return 2 * (i * i);
}
private static int multi(int i) {
return 2 * i * i;
}
Izvades rezultāts:
...
405 ms A 785527736
327 ms B 785527736
404 ms A 785527736
329 ms B 785527736
404 ms A 785527736
328 ms B 785527736
404 ms A 785527736
328 ms B 785527736
410 ms
333 ms
Tad kāpēc? Baita kods ir šāds:
private static multiB(int arg0) { // 2 * (i * i)
<localVar:index=0, name=i , desc=I, sig=null, start=L1, end=L2>
L1 {
iconst_2
iload0
iload0
imul
imul
ireturn
}
L2 {
}
}
private static multi(int arg0) { // 2 * i * i
<localVar:index=0, name=i , desc=I, sig=null, start=L1, end=L2>
L1 {
iconst_2
iload0
imul
iload0
imul
ireturn
}
L2 {
}
}
Atšķirība ir šāda:
Ar iekavām (2 * (i * i)
):
Bez iekavām (2 * i * i
):
Visu ielādēšana uz kaudzes un pēc tam darbība atpakaļ uz leju ir ātrāka nekā pārslēgšanās starp ievietošanu uz kaudzes un darbību uz tās.
Es saņēmu līdzīgus rezultātus:
2 * (i * i): 0.458765943 s, n=119860736
2 * i * i: 0.580255126 s, n=119860736
Es saņēmu SAME rezultātus, ja abas cilpas bija vienā programmā vai katra bija atsevišķā .java failā/.class, kas izpildīta atsevišķā palaišanas reizē.
Visbeidzot, šeit ir javap -c -v <.java>
dekompilācija:
3: ldc #3 // String 2 * (i * i):
5: invokevirtual #4 // Method java/io/PrintStream.print:(Ljava/lang/String;)V
8: invokestatic #5 // Method java/lang/System.nanoTime:()J
8: invokestatic #5 // Method java/lang/System.nanoTime:()J
11: lstore_1
12: iconst_0
13: istore_3
14: iconst_0
15: istore 4
17: iload 4
19: ldc #6 // int 1000000000
21: if_icmpge 40
24: iload_3
25: iconst_2
26: iload 4
28: iload 4
30: imul
31: imul
32: iadd
33: istore_3
34: iinc 4, 1
37: goto 17
vs.
3: ldc #3 // String 2 * i * i:
5: invokevirtual #4 // Method java/io/PrintStream.print:(Ljava/lang/String;)V
8: invokestatic #5 // Method java/lang/System.nanoTime:()J
11: lstore_1
12: iconst_0
13: istore_3
14: iconst_0
15: istore 4
17: iload 4
19: ldc #6 // int 1000000000
21: if_icmpge 40
24: iload_3
25: iconst_2
26: iload 4
28: imul
29: iload 4
31: imul
32: iadd
33: istore_3
34: iinc 4, 1
37: goto 17
FYI -
java -version
java version "1.8.0_121"
Java(TM) SE Runtime Environment (build 1.8.0_121-b13)
Java HotSpot(TM) 64-Bit Server VM (build 25.121-b13, mixed mode)
Abas pievienošanas metodes ģenerē nedaudz atšķirīgu baitu kodu:
17: iconst_2
18: iload 4
20: iload 4
22: imul
23: imul
24: iadd
Attiecībā uz 2 * (i * i)
pret:
17: iconst_2
18: iload 4
20: imul
21: iload 4
23: imul
24: iadd
Par 2 * i * i
.
Un, izmantojot šādu JMH etalonu:
@Warmup(iterations = 5, batchSize = 1)
@Measurement(iterations = 5, batchSize = 1)
@Fork(1)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
@State(Scope.Benchmark)
public class MyBenchmark {
@Benchmark
public int noBrackets() {
int n = 0;
for (int i = 0; i < 1000000000; i++) {
n += 2 * i * i;
}
return n;
}
@Benchmark
public int brackets() {
int n = 0;
for (int i = 0; i < 1000000000; i++) {
n += 2 * (i * i);
}
return n;
}
}
Atšķirība ir acīmredzama:
# JMH version: 1.21
# VM version: JDK 11, Java HotSpot(TM) 64-Bit Server VM, 11+28
# VM options: <none>
Benchmark (n) Mode Cnt Score Error Units
MyBenchmark.brackets 1000000000 avgt 5 380.889 ± 58.011 ms/op
MyBenchmark.noBrackets 1000000000 avgt 5 512.464 ± 11.098 ms/op
Tas, ko jūs novērojat, ir pareizi, nevis tikai jūsu salīdzinošās novērtēšanas stila anomālija (t.i., nav iesildīšanās, skat. Kā uzrakstīt pareizu mikromēģinājumu Java valodā?)).
Atkārtoti palaist ar Graal:
# JMH version: 1.21
# VM version: JDK 11, Java HotSpot(TM) 64-Bit Server VM, 11+28
# VM options: -XX:+UnlockExperimentalVMOptions -XX:+EnableJVMCI -XX:+UseJVMCICompiler
Benchmark (n) Mode Cnt Score Error Units
MyBenchmark.brackets 1000000000 avgt 5 335.100 ± 23.085 ms/op
MyBenchmark.noBrackets 1000000000 avgt 5 331.163 ± 50.670 ms/op
Jūs redzat, ka rezultāti ir daudz tuvāki, kas ir loģiski, jo Graal ir kopumā efektīvāks un modernāks kompilators.
Tātad tas patiesībā ir atkarīgs no tā, cik labi JIT kompilators spēj optimizēt konkrēto koda daļu, un tam ne vienmēr ir loģisks pamatojums.