为什么在Java中2*(i*i)比2*i*i快?

以下Java程序平均运行时间在0.50秒到0.55秒之间:

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

如果我用2 * i * i替换2 * (i * i),运行需要0.60到0.65秒。为什么?

我运行了每个版本的程序15次,在两者之间交替运行。以下是结果:

 2*(i*i)  │  2*i*i──────────┼──────────0.5183738 │ 0.62464340.5298337 │ 0.60497220.5308647 │ 0.66033630.5133458 │ 0.62433280.5003011 │ 0.65418020.5366181 │ 0.63126380.515149  │ 0.62411050.5237389 │ 0.6278150.5249942 │ 0.61142520.5641624 │ 0.67810330.538412  │ 0.63939690.5466744 │ 0.66088450.531159  │ 0.62010770.5048032 │ 0.65115590.5232789 │ 0.6544526

2 * i * i的最快运行时间比2 * (i * i)的最慢运行时间长。如果他们有相同的效率,这种情况发生的概率会小于1/2^15 * 100% = 0.00305%

252584 次浏览

两种加法方法确实会生成略有不同的字节码:

  17: iconst_218: iload         420: iload         422: imul23: imul24: iadd

对于2 * (i * i) vs:

  17: iconst_218: iload         420: imul21: iload         423: imul24: iadd

对于2 * i * i

当使用这样的JMH基准时:

@Warmup(iterations = 5, batchSize = 1)@Measurement(iterations = 5, batchSize = 1)@Fork(1)@BenchmarkMode(Mode.AverageTime)@OutputTimeUnit(TimeUnit.MILLISECONDS)@State(Scope.Benchmark)public class MyBenchmark {
@Benchmarkpublic int noBrackets() {int n = 0;for (int i = 0; i < 1000000000; i++) {n += 2 * i * i;}return n;}
@Benchmarkpublic int brackets() {int n = 0;for (int i = 0; i < 1000000000; i++) {n += 2 * (i * i);}return n;}
}

区别很明显:

# 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  UnitsMyBenchmark.brackets    1000000000  avgt    5  380.889 ± 58.011  ms/opMyBenchmark.noBrackets  1000000000  avgt    5  512.464 ± 11.098  ms/op

您观察到的是正确的,而不仅仅是基准测试风格的异常(即没有预热,请参阅如何在Java中编写正确的微基准测试?

再次使用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  UnitsMyBenchmark.brackets    1000000000  avgt    5  335.100 ± 23.085  ms/opMyBenchmark.noBrackets  1000000000  avgt    5  331.163 ± 50.670  ms/op

您可以看到结果更接近,这是有道理的,因为Graal是一个整体性能更好,更现代的编译器。

因此,这实际上只是取决于即时编译器能够优化特定代码的能力,并且不一定有逻辑原因。

我得到了类似的结果:

2 * (i * i): 0.458765943 s, n=1198607362 * i * i: 0.580255126 s, n=119860736

如果两个循环都在同一个程序中,或者每个循环都在单独的. java文件/. class中,在单独的运行中执行,我得到了相同的结果。

最后,这里是每个的javap -c -v <.java>反编译:

     3: ldc           #3                  // String 2 * (i * i):5: invokevirtual #4                  // Method java/io/PrintStream.print:(Ljava/lang/String;)V8: invokestatic  #5                  // Method java/lang/System.nanoTime:()J8: invokestatic  #5                  // Method java/lang/System.nanoTime:()J11: lstore_112: iconst_013: istore_314: iconst_015: istore        417: iload         419: ldc           #6                  // int 100000000021: if_icmpge     4024: iload_325: iconst_226: iload         428: iload         430: imul31: imul32: iadd33: istore_334: iinc          4, 137: goto          17

vs.

     3: ldc           #3                  // String 2 * i * i:5: invokevirtual #4                  // Method java/io/PrintStream.print:(Ljava/lang/String;)V8: invokestatic  #5                  // Method java/lang/System.nanoTime:()J11: lstore_112: iconst_013: istore_314: iconst_015: istore        417: iload         419: ldc           #6                  // int 100000000021: if_icmpge     4024: iload_325: iconst_226: iload         428: imul29: iload         431: imul32: iadd33: istore_334: iinc          4, 137: goto          17

仅供参考

java -versionjava 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)

字节码:https://cs.nyu.edu/courses/fall00/V22.0201-001/jvm2.html字节码查看器:https://github.com/Konloch/bytecode-viewer

在我的JDK(Windows 10 64位,1.80_65-b17)上,我可以重现并解释:

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

输出:

...405 ms A 785527736327 ms B 785527736404 ms A 785527736329 ms B 785527736404 ms A 785527736328 ms B 785527736404 ms A 785527736328 ms B 785527736410 ms333 ms

为什么?字节码是这样的:

 private static multiB(int arg0) { // 2 * (i * i)<localVar:index=0, name=i , desc=I, sig=null, start=L1, end=L2>
L1 {iconst_2iload0iload0imulimulireturn}L2 {}}
private static multi(int arg0) { // 2 * i * i<localVar:index=0, name=i , desc=I, sig=null, start=L1, end=L2>
L1 {iconst_2iload0imuliload0imulireturn}L2 {}}

区别是:带括号(2 * (i * i)):

  • 推送常量栈
  • 将本地数据推送到堆栈
  • 将本地数据推送到堆栈
  • 乘以栈顶
  • 乘以栈顶

不带括号(2 * i * i):

  • 推送常量栈
  • 将本地数据推送到堆栈
  • 乘以栈顶
  • 将本地数据推送到堆栈
  • 乘以栈顶

在堆栈上加载所有内容,然后返回工作,比在堆栈上放置和操作之间切换要快。

(编者注:这个答案与另一个答案所显示的从观察asm的证据相矛盾。这是一个由一些实验支持的猜测,但事实证明它不正确。)


当乘法为2 * (i * i)时,JVM能够从循环中分解出乘法2,从而产生等效但更有效的代码:

int n = 0;for (int i = 0; i < 1000000000; i++) {n += i * i;}n *= 2;

但是当乘法为(2 * i) * i时,JVM不会对其进行优化,因为常量的乘法在n +=加法之前不再正确。

以下是我认为是这种情况的几个原因:

  • 在循环开始时添加if (n == 0) n = 1语句会导致两个版本同样高效,因为分解乘法不再保证结果相同
  • 优化版本(通过分解乘以2)与2 * (i * i)版本完全一样快

这是我用来得出这些结论的测试代码:

public static void main(String[] args) {long fastVersion = 0;long slowVersion = 0;long optimizedVersion = 0;long modifiedFastVersion = 0;long modifiedSlowVersion = 0;
for (int i = 0; i < 10; i++) {fastVersion += fastVersion();slowVersion += slowVersion();optimizedVersion += optimizedVersion();modifiedFastVersion += modifiedFastVersion();modifiedSlowVersion += modifiedSlowVersion();}
System.out.println("Fast version: " + (double) fastVersion / 1000000000 + " s");System.out.println("Slow version: " + (double) slowVersion / 1000000000 + " s");System.out.println("Optimized version: " + (double) optimizedVersion / 1000000000 + " s");System.out.println("Modified fast version: " + (double) modifiedFastVersion / 1000000000 + " s");System.out.println("Modified slow version: " + (double) modifiedSlowVersion / 1000000000 + " s");}
private static long fastVersion() {long startTime = System.nanoTime();int n = 0;for (int i = 0; i < 1000000000; i++) {n += 2 * (i * i);}return System.nanoTime() - startTime;}
private static long slowVersion() {long startTime = System.nanoTime();int n = 0;for (int i = 0; i < 1000000000; i++) {n += 2 * i * i;}return System.nanoTime() - startTime;}
private static long optimizedVersion() {long startTime = System.nanoTime();int n = 0;for (int i = 0; i < 1000000000; i++) {n += i * i;}n *= 2;return System.nanoTime() - startTime;}
private static long modifiedFastVersion() {long startTime = System.nanoTime();int n = 0;for (int i = 0; i < 1000000000; i++) {if (n == 0) n = 1;n += 2 * (i * i);}return System.nanoTime() - startTime;}
private static long modifiedSlowVersion() {long startTime = System.nanoTime();int n = 0;for (int i = 0; i < 1000000000; i++) {if (n == 0) n = 1;n += 2 * i * i;}return System.nanoTime() - startTime;}

结果如下:

Fast version: 5.7274411 sSlow version: 7.6190804 sOptimized version: 5.1348007 sModified fast version: 7.1492705 sModified slow version: 7.2952668 s

我尝试了使用默认原型的JMH:我还添加了一个基于Runemoro的解释的优化版本。

@State(Scope.Benchmark)@Warmup(iterations = 2)@Fork(1)@Measurement(iterations = 10)@OutputTimeUnit(TimeUnit.NANOSECONDS)//@BenchmarkMode({ Mode.All })@BenchmarkMode(Mode.AverageTime)public class MyBenchmark {@Param({ "100", "1000", "1000000000" })private int size;
@Benchmarkpublic int two_square_i() {int n = 0;for (int i = 0; i < size; i++) {n += 2 * (i * i);}return n;}
@Benchmarkpublic int square_i_two() {int n = 0;for (int i = 0; i < size; i++) {n += i * i;}return 2*n;}
@Benchmarkpublic int two_i_() {int n = 0;for (int i = 0; i < size; i++) {n += 2 * i * i;}return n;}}

结果在这里:

Benchmark                           (size)  Mode  Samples          Score   Score error  Unitso.s.MyBenchmark.square_i_two           100  avgt       10         58,062         1,410  ns/opo.s.MyBenchmark.square_i_two          1000  avgt       10        547,393        12,851  ns/opo.s.MyBenchmark.square_i_two    1000000000  avgt       10  540343681,267  16795210,324  ns/opo.s.MyBenchmark.two_i_                 100  avgt       10         87,491         2,004  ns/opo.s.MyBenchmark.two_i_                1000  avgt       10       1015,388        30,313  ns/opo.s.MyBenchmark.two_i_          1000000000  avgt       10  967100076,600  24929570,556  ns/opo.s.MyBenchmark.two_square_i           100  avgt       10         70,715         2,107  ns/opo.s.MyBenchmark.two_square_i          1000  avgt       10        686,977        24,613  ns/opo.s.MyBenchmark.two_square_i    1000000000  avgt       10  652736811,450  27015580,488  ns/op

在我的PC上(酷睿i7 860-除了在我的智能手机上阅读外,它什么也不做):

  • n += i*i那么n*2是第一个
  • 2 * (i * i)是第二个。

JVM显然没有以与人类相同的方式进行优化(基于Runemoro的回答)。

现在,读取字节码:javap -c -v ./target/classes/org/sample/MyBenchmark.class

我不是字节码专家,但我们iload_2之前我们imul:这可能是你得到的区别:我可以假设JVM优化读取i两次(i已经在这里,没有必要再次加载它),而在2*i*i它不能。

字节码的顺序略有不同。

2 * (i * i)

     iconst_2iload0iload0imulimuliadd

vs2 * i * i

     iconst_2iload0imuliload0imuliadd

乍一看,这不应该有什么区别;如果有的话,第二个版本更理想,因为它少用了一个插槽。

因此,我们需要深入挖掘较低级别(JIT)1

请记住,JIT倾向于非常积极地展开小循环。事实上,我们观察到2 * (i * i)情况下的16倍展开:

030   B2: # B2 B3 <- B1 B2  Loop: B2-B2 inner main of N18 Freq: 1e+006030     addl    R11, RBP    # int033     movl    RBP, R13    # spill036     addl    RBP, #14    # int039     imull   RBP, RBP    # int03c     movl    R9, R13 # spill03f     addl    R9, #13 # int043     imull   R9, R9  # int047     sall    RBP, #1049     sall    R9, #104c     movl    R8, R13 # spill04f     addl    R8, #15 # int053     movl    R10, R8 # spill056     movdl   XMM1, R8    # spill05b     imull   R10, R8 # int05f     movl    R8, R13 # spill062     addl    R8, #12 # int066     imull   R8, R8  # int06a     sall    R10, #106d     movl    [rsp + #32], R10    # spill072     sall    R8, #1075     movl    RBX, R13    # spill078     addl    RBX, #11    # int07b     imull   RBX, RBX    # int07e     movl    RCX, R13    # spill081     addl    RCX, #10    # int084     imull   RCX, RCX    # int087     sall    RBX, #1089     sall    RCX, #108b     movl    RDX, R13    # spill08e     addl    RDX, #8 # int091     imull   RDX, RDX    # int094     movl    RDI, R13    # spill097     addl    RDI, #7 # int09a     imull   RDI, RDI    # int09d     sall    RDX, #109f     sall    RDI, #10a1     movl    RAX, R13    # spill0a4     addl    RAX, #6 # int0a7     imull   RAX, RAX    # int0aa     movl    RSI, R13    # spill0ad     addl    RSI, #4 # int0b0     imull   RSI, RSI    # int0b3     sall    RAX, #10b5     sall    RSI, #10b7     movl    R10, R13    # spill0ba     addl    R10, #2 # int0be     imull   R10, R10    # int0c2     movl    R14, R13    # spill0c5     incl    R14 # int0c8     imull   R14, R14    # int0cc     sall    R10, #10cf     sall    R14, #10d2     addl    R14, R11    # int0d5     addl    R14, R10    # int0d8     movl    R10, R13    # spill0db     addl    R10, #3 # int0df     imull   R10, R10    # int0e3     movl    R11, R13    # spill0e6     addl    R11, #5 # int0ea     imull   R11, R11    # int0ee     sall    R10, #10f1     addl    R10, R14    # int0f4     addl    R10, RSI    # int0f7     sall    R11, #10fa     addl    R11, R10    # int0fd     addl    R11, RAX    # int100     addl    R11, RDI    # int103     addl    R11, RDX    # int106     movl    R10, R13    # spill109     addl    R10, #9 # int10d     imull   R10, R10    # int111     sall    R10, #1114     addl    R10, R11    # int117     addl    R10, RCX    # int11a     addl    R10, RBX    # int11d     addl    R10, R8 # int120     addl    R9, R10 # int123     addl    RBP, R9 # int126     addl    RBP, [RSP + #32 (32-bit)]   # int12a     addl    R13, #16    # int12e     movl    R11, R13    # spill131     imull   R11, R13    # int135     sall    R11, #1138     cmpl    R13, #99999998513f     jl     B2   # loop end  P=1.000000 C=6554623.000000

我们看到有1个寄存器“溢出”到堆栈上。

对于2 * i * i版本:

05a   B3: # B2 B4 <- B1 B2  Loop: B3-B2 inner main of N18 Freq: 1e+00605a     addl    RBX, R11    # int05d     movl    [rsp + #32], RBX    # spill061     movl    R11, R8 # spill064     addl    R11, #15    # int068     movl    [rsp + #36], R11    # spill06d     movl    R11, R8 # spill070     addl    R11, #14    # int074     movl    R10, R9 # spill077     addl    R10, #16    # int07b     movdl   XMM2, R10   # spill080     movl    RCX, R9 # spill083     addl    RCX, #14    # int086     movdl   XMM1, RCX   # spill08a     movl    R10, R9 # spill08d     addl    R10, #12    # int091     movdl   XMM4, R10   # spill096     movl    RCX, R9 # spill099     addl    RCX, #10    # int09c     movdl   XMM6, RCX   # spill0a0     movl    RBX, R9 # spill0a3     addl    RBX, #8 # int0a6     movl    RCX, R9 # spill0a9     addl    RCX, #6 # int0ac     movl    RDX, R9 # spill0af     addl    RDX, #4 # int0b2     addl    R9, #2  # int0b6     movl    R10, R14    # spill0b9     addl    R10, #22    # int0bd     movdl   XMM3, R10   # spill0c2     movl    RDI, R14    # spill0c5     addl    RDI, #20    # int0c8     movl    RAX, R14    # spill0cb     addl    RAX, #32    # int0ce     movl    RSI, R14    # spill0d1     addl    RSI, #18    # int0d4     movl    R13, R14    # spill0d7     addl    R13, #24    # int0db     movl    R10, R14    # spill0de     addl    R10, #26    # int0e2     movl    [rsp + #40], R10    # spill0e7     movl    RBP, R14    # spill0ea     addl    RBP, #28    # int0ed     imull   RBP, R11    # int0f1     addl    R14, #30    # int0f5     imull   R14, [RSP + #36 (32-bit)]   # int0fb     movl    R10, R8 # spill0fe     addl    R10, #11    # int102     movdl   R11, XMM3   # spill107     imull   R11, R10    # int10b     movl    [rsp + #44], R11    # spill110     movl    R10, R8 # spill113     addl    R10, #10    # int117     imull   RDI, R10    # int11b     movl    R11, R8 # spill11e     addl    R11, #8 # int122     movdl   R10, XMM2   # spill127     imull   R10, R11    # int12b     movl    [rsp + #48], R10    # spill130     movl    R10, R8 # spill133     addl    R10, #7 # int137     movdl   R11, XMM1   # spill13c     imull   R11, R10    # int140     movl    [rsp + #52], R11    # spill145     movl    R11, R8 # spill148     addl    R11, #6 # int14c     movdl   R10, XMM4   # spill151     imull   R10, R11    # int155     movl    [rsp + #56], R10    # spill15a     movl    R10, R8 # spill15d     addl    R10, #5 # int161     movdl   R11, XMM6   # spill166     imull   R11, R10    # int16a     movl    [rsp + #60], R11    # spill16f     movl    R11, R8 # spill172     addl    R11, #4 # int176     imull   RBX, R11    # int17a     movl    R11, R8 # spill17d     addl    R11, #3 # int181     imull   RCX, R11    # int185     movl    R10, R8 # spill188     addl    R10, #2 # int18c     imull   RDX, R10    # int190     movl    R11, R8 # spill193     incl    R11 # int196     imull   R9, R11 # int19a     addl    R9, [RSP + #32 (32-bit)]    # int19f     addl    R9, RDX # int1a2     addl    R9, RCX # int1a5     addl    R9, RBX # int1a8     addl    R9, [RSP + #60 (32-bit)]    # int1ad     addl    R9, [RSP + #56 (32-bit)]    # int1b2     addl    R9, [RSP + #52 (32-bit)]    # int1b7     addl    R9, [RSP + #48 (32-bit)]    # int1bc     movl    R10, R8 # spill1bf     addl    R10, #9 # int1c3     imull   R10, RSI    # int1c7     addl    R10, R9 # int1ca     addl    R10, RDI    # int1cd     addl    R10, [RSP + #44 (32-bit)]   # int1d2     movl    R11, R8 # spill1d5     addl    R11, #12    # int1d9     imull   R13, R11    # int1dd     addl    R13, R10    # int1e0     movl    R10, R8 # spill1e3     addl    R10, #13    # int1e7     imull   R10, [RSP + #40 (32-bit)]   # int1ed     addl    R10, R13    # int1f0     addl    RBP, R10    # int1f3     addl    R14, RBP    # int1f6     movl    R10, R8 # spill1f9     addl    R10, #16    # int1fd     cmpl    R10, #999999985204     jl     B2   # loop end  P=1.000000 C=7419903.000000

在这里,我们观察到更多的“溢出”和对堆栈[RSP + ...]的更多访问,因为需要保留更多的中间结果。

因此,这个问题的答案很简单:2 * (i * i)2 * i * i快,因为JIT为第一种情况生成了更多的最佳汇编代码。


当然,第一个版本和第二个版本都不好;循环可以真正受益于向量化,因为任何x86-64 CPU都至少支持SSE2。

因此,这是优化器的问题;通常情况下,它过于积极地展开并射击自己的脚,同时错过了各种其他机会。

事实上,现代x86-64 CPU将指令进一步分解为微操作(µops),并且具有寄存器重命名、µop缓存和循环缓冲区等功能,循环优化比简单的展开需要更多的技巧来获得最佳性能。根据Agner Fog的优化指南

μop缓存带来的性能提升可能相当大如果平均指令长度超过4个字节,则相当可观。以下优化μop缓存使用的方法可以注意:

  • 确保关键循环足够小,可以放入µop缓存。
  • 将最关键的循环条目和函数条目对齐32。
  • 避免不必要的循环展开。
  • 避免有额外加载时间的指令
    . . .

关于那些加载时间-即使最快的L1D命中也需要4个周期,一个额外的寄存器和µop,所以是的,即使是对内存的几次访问也会损害紧密循环中的性能。

但是回到矢量化的机会-看看它有多快,我们可以用GCC编译类似的C应用程序,它完全矢量化了它(显示了AVX2,SSE2类似)2

  vmovdqa ymm0, YMMWORD PTR .LC0[rip]vmovdqa ymm3, YMMWORD PTR .LC1[rip]xor eax, eaxvpxor xmm2, xmm2, xmm2.L2:vpmulld ymm1, ymm0, ymm0inc eaxvpaddd ymm0, ymm0, ymm3vpslld ymm1, ymm1, 1vpaddd ymm2, ymm2, ymm1cmp eax, 125000000      ; 8 calculations per iterationjne .L2vmovdqa xmm0, xmm2vextracti128 xmm2, ymm2, 1vpaddd xmm2, xmm0, xmm2vpsrldq xmm0, xmm2, 8vpaddd xmm0, xmm2, xmm0vpsrldq xmm1, xmm0, 4vpaddd xmm0, xmm0, xmm1vmovd eax, xmm0vzeroupper

使用运行时间:

  • 上证指数:0.24秒,或2倍快。
  • AVX:0.15秒,或3倍快。
  • AVX2:0.08秒,或快5倍。

1要获得JIT生成的程序集输出,获取一个调试JVM并使用#0运行

2C版本使用#0标志编译,这使GCC能够将有符号整数溢出视为二进制补码环绕。

Kasperd在接受的答案的评论中问道:

Java和C示例使用完全不同的寄存器名称。两个示例都使用AMD64 ISA吗?

xor edx, edxxor eax, eax.L2:mov ecx, edximul ecx, edxadd edx, 1lea eax, [rax+rcx*2]cmp edx, 1000000000jne .L2

我没有足够的声誉在评论中回答这个问题,但这些都是相同的ISA。值得指出的是,GCC版本使用32位整数逻辑,JVM编译版本内部使用64位整数逻辑。

R8到R15只是新X86_64登记册。EAX到EDX是RAX到RDX通用寄存器的较低部分。答案中的重要部分是GCC版本没有展开。它只是每个实际的机器代码循环执行一轮循环。而JVM版本在一个物理循环中有16轮循环(基于rustyx的答案,我没有重新解释程序集)。这是使用更多寄存器的原因之一,因为循环主体实际上长了16倍。

虽然与问题的环境没有直接关系,但出于好奇,我在. NET Core 2.1,x64,发布模式上进行了相同的测试。

这是有趣的结果,证实了类似的现象(另一种方式)发生在原力的黑暗面。代码:

static void Main(string[] args){Stopwatch watch = new Stopwatch();
Console.WriteLine("2 * (i * i)");
for (int a = 0; a < 10; a++){int n = 0;
watch.Restart();
for (int i = 0; i < 1000000000; i++){n += 2 * (i * i);}
watch.Stop();
Console.WriteLine($"result:{n}, {watch.ElapsedMilliseconds} ms");}
Console.WriteLine();Console.WriteLine("2 * i * i");
for (int a = 0; a < 10; a++){int n = 0;
watch.Restart();
for (int i = 0; i < 1000000000; i++){n += 2 * i * i;}
watch.Stop();
Console.WriteLine($"result:{n}, {watch.ElapsedMilliseconds}ms");}}

结果:

2*(i*i)

  • 结果:119860736,438 ms
  • 结果:119860736,433 ms
  • 结果:119860736,437 ms
  • 结果:119860736,435 ms
  • 结果:119860736,436 ms
  • 结果:119860736,435 ms
  • 结果:119860736,435 ms
  • 结果:119860736,439 ms
  • 结果:119860736,436 ms
  • 结果:119860736,437 ms

2*i*i

  • 结果:119860736,417 ms
  • 结果:119860736,417 ms
  • 结果:119860736,417 ms
  • 结果:119860736,418 ms
  • 结果:119860736,418 ms
  • 结果:119860736,417 ms
  • 结果:119860736,418 ms
  • 结果:119860736,416 ms
  • 结果:119860736,417 ms
  • 结果:119860736,418 ms

我确实使用IBM最新的Java8 JVM重现了这个实验:

java version "1.8.0_191"Java(TM) 2 Runtime Environment, Standard Edition (IBM build 1.8.0_191-b12 26_Oct_2018_18_45 Mac OS X x64(SR5 FP25))Java HotSpot(TM) 64-Bit Server VM (build 25.191-b12, mixed mode)

这显示了非常相似的结果:

0.374653912 sn = 1198607360.447778698 sn = 119860736

(第二个结果使用2*i*i)。

有趣的是,在同一台机器上运行,但使用OracleJava:

Java version "1.8.0_181"Java(TM) SE Runtime Environment (build 1.8.0_181-b13)Java HotSpot(TM) 64-Bit Server VM (build 25.181-b13, mixed mode)

结果平均有点慢:

0.414331815 sn = 1198607360.491430656 sn = 119860736

长话短说:即使是HotSpot的小版本号也很重要,因为JIT实现中的细微差异也会产生显着的影响。

使用Java11并使用以下VM选项关闭循环展开的有趣观察:

-XX:LoopUnrollLimit=0

2 * (i * i)表达式的循环导致更紧凑的本机代码1

L0001: add    eax,r11dinc    r8dmov    r11d,r8dimul   r11d,r8dshl    r11d,1hcmp    r8d,r10djl     L0001

2 * i * i版本相比:

L0001: add    eax,r11dmov    r11d,r8dshl    r11d,1hadd    r11d,2hinc    r8dimul   r11d,r8dcmp    r8d,r10djl     L0001

Java版本:

java version "11" 2018-09-25Java(TM) SE Runtime Environment 18.9 (build 11+28)Java HotSpot(TM) 64-Bit Server VM 18.9 (build 11+28, mixed mode)

基准结果:

Benchmark          (size)  Mode  Cnt    Score     Error  UnitsLoopTest.fast  1000000000  avgt    5  694,868 ±  36,470  ms/opLoopTest.slow  1000000000  avgt    5  769,840 ± 135,006  ms/op

基准源代码:

@BenchmarkMode(Mode.AverageTime)@OutputTimeUnit(TimeUnit.MILLISECONDS)@Warmup(iterations = 5, time = 5, timeUnit = TimeUnit.SECONDS)@Measurement(iterations = 5, time = 5, timeUnit = TimeUnit.SECONDS)@State(Scope.Thread)@Fork(1)public class LoopTest {
@Param("1000000000") private int size;
public static void main(String[] args) throws RunnerException {Options opt = new OptionsBuilder().include(LoopTest.class.getSimpleName()).jvmArgs("-XX:LoopUnrollLimit=0").build();new Runner(opt).run();}
@Benchmarkpublic int slow() {int n = 0;for (int i = 0; i < size; i++)n += 2 * i * i;return n;}
@Benchmarkpublic int fast() {int n = 0;for (int i = 0; i < size; i++)n += 2 * (i * i);return n;}}

1-使用的VM选项:-XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -XX:LoopUnrollLimit=0