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

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/53452713/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-11 00:48:35  来源:igfitidea点击:

Why is 2 * (i * i) faster than 2 * i * i in Java?

javaperformancebenchmarkingbytecodejit

提问by Stefan

The following Java program takes on average between 0.50 secs and 0.55 secs to run:

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

If I replace 2 * (i * i)with 2 * i * i, it takes between 0.60 and 0.65 secs to run. How come?

如果我替换2 * (i * i)2 * i * i,则运行需要 0.60 到 0.65 秒。怎么来的?

I ran each version of the program 15 times, alternating between the two. Here are the results:

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

 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

The fastest run of 2 * i * itook longer than the slowest run of 2 * (i * i). If they had the same efficiency, the probability of this happening would be less than 1/2^15 * 100% = 0.00305%.

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

采纳答案by rustyx

There is a slight difference in the ordering of the bytecode.

字节码的顺序略有不同。

2 * (i * i):

2 * (i * i)

     iconst_2
     iload0
     iload0
     imul
     imul
     iadd

vs 2 * i * i:

对比2 * i * i

     iconst_2
     iload0
     imul
     iload0
     imul
     iadd

At first sight this should not make a difference; if anything the second version is more optimal since it uses one slot less.

乍一看,这应该没什么区别。如果有的话,第二个版本更优化,因为它少使用一个插槽。

So we need to dig deeper into the lower level (JIT)1.

所以我们需要深入挖掘底层(JIT)1

Remember that JIT tends to unroll small loops very aggressively. Indeed we observe a 16x unrolling for the 2 * (i * i)case:

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

030   B2: # B2 B3 <- B1 B2  Loop: B2-B2 inner main of N18 Freq: 1e+006
030     addl    R11, RBP    # int
033     movl    RBP, R13    # spill
036     addl    RBP, #14    # int
039     imull   RBP, RBP    # int
03c     movl    R9, R13 # spill
03f     addl    R9, #13 # int
043     imull   R9, R9  # int
047     sall    RBP, #1
049     sall    R9, #1
04c     movl    R8, R13 # spill
04f     addl    R8, #15 # int
053     movl    R10, R8 # spill
056     movdl   XMM1, R8    # spill
05b     imull   R10, R8 # int
05f     movl    R8, R13 # spill
062     addl    R8, #12 # int
066     imull   R8, R8  # int
06a     sall    R10, #1
06d     movl    [rsp + #32], R10    # spill
072     sall    R8, #1
075     movl    RBX, R13    # spill
078     addl    RBX, #11    # int
07b     imull   RBX, RBX    # int
07e     movl    RCX, R13    # spill
081     addl    RCX, #10    # int
084     imull   RCX, RCX    # int
087     sall    RBX, #1
089     sall    RCX, #1
08b     movl    RDX, R13    # spill
08e     addl    RDX, #8 # int
091     imull   RDX, RDX    # int
094     movl    RDI, R13    # spill
097     addl    RDI, #7 # int
09a     imull   RDI, RDI    # int
09d     sall    RDX, #1
09f     sall    RDI, #1
0a1     movl    RAX, R13    # spill
0a4     addl    RAX, #6 # int
0a7     imull   RAX, RAX    # int
0aa     movl    RSI, R13    # spill
0ad     addl    RSI, #4 # int
0b0     imull   RSI, RSI    # int
0b3     sall    RAX, #1
0b5     sall    RSI, #1
0b7     movl    R10, R13    # spill
0ba     addl    R10, #2 # int
0be     imull   R10, R10    # int
0c2     movl    R14, R13    # spill
0c5     incl    R14 # int
0c8     imull   R14, R14    # int
0cc     sall    R10, #1
0cf     sall    R14, #1
0d2     addl    R14, R11    # int
0d5     addl    R14, R10    # int
0d8     movl    R10, R13    # spill
0db     addl    R10, #3 # int
0df     imull   R10, R10    # int
0e3     movl    R11, R13    # spill
0e6     addl    R11, #5 # int
0ea     imull   R11, R11    # int
0ee     sall    R10, #1
0f1     addl    R10, R14    # int
0f4     addl    R10, RSI    # int
0f7     sall    R11, #1
0fa     addl    R11, R10    # int
0fd     addl    R11, RAX    # int
100     addl    R11, RDI    # int
103     addl    R11, RDX    # int
106     movl    R10, R13    # spill
109     addl    R10, #9 # int
10d     imull   R10, R10    # int
111     sall    R10, #1
114     addl    R10, R11    # int
117     addl    R10, RCX    # int
11a     addl    R10, RBX    # int
11d     addl    R10, R8 # int
120     addl    R9, R10 # int
123     addl    RBP, R9 # int
126     addl    RBP, [RSP + #32 (32-bit)]   # int
12a     addl    R13, #16    # int
12e     movl    R11, R13    # spill
131     imull   R11, R13    # int
135     sall    R11, #1
138     cmpl    R13, #999999985
13f     jl     B2   # loop end  P=1.000000 C=6554623.000000

We see that there is 1 register that is "spilled" onto the stack.

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

And for the 2 * i * iversion:

对于2 * i * i版本:

05a   B3: # B2 B4 <- B1 B2  Loop: B3-B2 inner main of N18 Freq: 1e+006
05a     addl    RBX, R11    # int
05d     movl    [rsp + #32], RBX    # spill
061     movl    R11, R8 # spill
064     addl    R11, #15    # int
068     movl    [rsp + #36], R11    # spill
06d     movl    R11, R8 # spill
070     addl    R11, #14    # int
074     movl    R10, R9 # spill
077     addl    R10, #16    # int
07b     movdl   XMM2, R10   # spill
080     movl    RCX, R9 # spill
083     addl    RCX, #14    # int
086     movdl   XMM1, RCX   # spill
08a     movl    R10, R9 # spill
08d     addl    R10, #12    # int
091     movdl   XMM4, R10   # spill
096     movl    RCX, R9 # spill
099     addl    RCX, #10    # int
09c     movdl   XMM6, RCX   # spill
0a0     movl    RBX, R9 # spill
0a3     addl    RBX, #8 # int
0a6     movl    RCX, R9 # spill
0a9     addl    RCX, #6 # int
0ac     movl    RDX, R9 # spill
0af     addl    RDX, #4 # int
0b2     addl    R9, #2  # int
0b6     movl    R10, R14    # spill
0b9     addl    R10, #22    # int
0bd     movdl   XMM3, R10   # spill
0c2     movl    RDI, R14    # spill
0c5     addl    RDI, #20    # int
0c8     movl    RAX, R14    # spill
0cb     addl    RAX, #32    # int
0ce     movl    RSI, R14    # spill
0d1     addl    RSI, #18    # int
0d4     movl    R13, R14    # spill
0d7     addl    R13, #24    # int
0db     movl    R10, R14    # spill
0de     addl    R10, #26    # int
0e2     movl    [rsp + #40], R10    # spill
0e7     movl    RBP, R14    # spill
0ea     addl    RBP, #28    # int
0ed     imull   RBP, R11    # int
0f1     addl    R14, #30    # int
0f5     imull   R14, [RSP + #36 (32-bit)]   # int
0fb     movl    R10, R8 # spill
0fe     addl    R10, #11    # int
102     movdl   R11, XMM3   # spill
107     imull   R11, R10    # int
10b     movl    [rsp + #44], R11    # spill
110     movl    R10, R8 # spill
113     addl    R10, #10    # int
117     imull   RDI, R10    # int
11b     movl    R11, R8 # spill
11e     addl    R11, #8 # int
122     movdl   R10, XMM2   # spill
127     imull   R10, R11    # int
12b     movl    [rsp + #48], R10    # spill
130     movl    R10, R8 # spill
133     addl    R10, #7 # int
137     movdl   R11, XMM1   # spill
13c     imull   R11, R10    # int
140     movl    [rsp + #52], R11    # spill
145     movl    R11, R8 # spill
148     addl    R11, #6 # int
14c     movdl   R10, XMM4   # spill
151     imull   R10, R11    # int
155     movl    [rsp + #56], R10    # spill
15a     movl    R10, R8 # spill
15d     addl    R10, #5 # int
161     movdl   R11, XMM6   # spill
166     imull   R11, R10    # int
16a     movl    [rsp + #60], R11    # spill
16f     movl    R11, R8 # spill
172     addl    R11, #4 # int
176     imull   RBX, R11    # int
17a     movl    R11, R8 # spill
17d     addl    R11, #3 # int
181     imull   RCX, R11    # int
185     movl    R10, R8 # spill
188     addl    R10, #2 # int
18c     imull   RDX, R10    # int
190     movl    R11, R8 # spill
193     incl    R11 # int
196     imull   R9, R11 # int
19a     addl    R9, [RSP + #32 (32-bit)]    # int
19f     addl    R9, RDX # int
1a2     addl    R9, RCX # int
1a5     addl    R9, RBX # int
1a8     addl    R9, [RSP + #60 (32-bit)]    # int
1ad     addl    R9, [RSP + #56 (32-bit)]    # int
1b2     addl    R9, [RSP + #52 (32-bit)]    # int
1b7     addl    R9, [RSP + #48 (32-bit)]    # int
1bc     movl    R10, R8 # spill
1bf     addl    R10, #9 # int
1c3     imull   R10, RSI    # int
1c7     addl    R10, R9 # int
1ca     addl    R10, RDI    # int
1cd     addl    R10, [RSP + #44 (32-bit)]   # int
1d2     movl    R11, R8 # spill
1d5     addl    R11, #12    # int
1d9     imull   R13, R11    # int
1dd     addl    R13, R10    # int
1e0     movl    R10, R8 # spill
1e3     addl    R10, #13    # int
1e7     imull   R10, [RSP + #40 (32-bit)]   # int
1ed     addl    R10, R13    # int
1f0     addl    RBP, R10    # int
1f3     addl    R14, RBP    # int
1f6     movl    R10, R8 # spill
1f9     addl    R10, #16    # int
1fd     cmpl    R10, #999999985
204     jl     B2   # loop end  P=1.000000 C=7419903.000000

Here we observe much more "spilling" and more accesses to the stack [RSP + ...], due to more intermediate results that need to be preserved.

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

Thus the answer to the question is simple: 2 * (i * i)is faster than 2 * i * ibecause the JIT generates more optimal assembly code for the first case.

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



But of course it is obvious that neither the first nor the second version is any good; the loop could really benefit from vectorization, since any x86-64 CPU has at least SSE2 support.

但当然很明显,第一个和第二个版本都没有任何好处;循环确实可以从矢量化中受益,因为任何 x86-64 CPU 都至少支持 SSE2。

So it's an issue of the optimizer; as is often the case, it unrolls too aggressively and shoots itself in the foot, all the while missing out on various other opportunities.

所以这是优化器的问题;通常情况下,它展开过于激进并用脚射击自己,同时错过了其他各种机会。

In fact, modern x86-64 CPUs break down the instructions further into micro-ops (µops) and with features like register renaming, µop caches and loop buffers, loop optimization takes a lot more finesse than a simple unrolling for optimal performance. According to Agner Fog's optimization guide:

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

The gain in performance due to the µop cache can be quite considerable if the average instruction length is more than 4 bytes. The following methods of optimizing the use of the µop cache may be considered:

  • Make sure that critical loops are small enough to fit into the μop cache.
  • Align the most critical loop entries and function entries by 32.
  • Avoid unnecessary loop unrolling.
  • Avoid instructions that have extra load time
    . . .

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

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

Regarding those load times - even the fastest L1D hit costs 4 cycles, an extra register and µop, so yes, even a few accesses to memory will hurt performance in tight loops.

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

But back to the vectorization opportunity - to see how fast it can be, we can compile a similar C application with GCC, which outright vectorizes it (AVX2 is shown, SSE2 is similar)2:

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

  vmovdqa ymm0, YMMWORD PTR .LC0[rip]
  vmovdqa ymm3, YMMWORD PTR .LC1[rip]
  xor eax, eax
  vpxor xmm2, xmm2, xmm2
.L2:
  vpmulld ymm1, ymm0, ymm0
  inc eax
  vpaddd ymm0, ymm0, ymm3
  vpslld ymm1, ymm1, 1
  vpaddd ymm2, ymm2, ymm1
  cmp eax, 125000000      ; 8 calculations per iteration
  jne .L2
  vmovdqa xmm0, xmm2
  vextracti128 xmm2, ymm2, 1
  vpaddd xmm2, xmm0, xmm2
  vpsrldq xmm0, xmm2, 8
  vpaddd xmm0, xmm2, xmm0
  vpsrldq xmm1, xmm0, 4
  vpaddd xmm0, xmm0, xmm1
  vmovd eax, xmm0
  vzeroupper

With run times:

随着运行时间:

  • SSE: 0.24 s, or 2 times faster.
  • AVX: 0.15 s, or 3 times faster.
  • AVX2: 0.08 s, or 5 times faster.
  • SSE:0.24 秒,或快 2 倍。
  • AVX:0.15 秒,或快 3 倍。
  • AVX2:0.08 秒,或快 5 倍。


1To get JIT generated assembly output, get a debug JVMand run with -XX:+PrintOptoAssembly

1要获得 JIT 生成的程序集输出,请获取调试 JVM并运行-XX:+PrintOptoAssembly

2The C version is compiled with the -fwrapvflag, which enables GCC to treat signed integer overflow as a two's-complement wrap-around.

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

回答by Jorn Vernee

The two methods of adding do generate slightly different byte code:

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

  17: iconst_2
  18: iload         4
  20: iload         4
  22: imul
  23: imul
  24: iadd

For 2 * (i * i)vs:

对于2 * (i * i)vs:

  17: iconst_2
  18: iload         4
  20: imul
  21: iload         4
  23: imul
  24: iadd

For 2 * i * i.

对于2 * i * i.

And when using a JMHbenchmark like this:

当使用这样的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 {

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

}

The difference is clear:

区别很明显:

# 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

What you observe is correct, and not just an anomaly of your benchmarking style (i.e. no warmup, see How do I write a correct micro-benchmark in Java?)

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

Running again with Graal:

再次使用 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

You see that the results are much closer, which makes sense, since Graal is an overall better performing, more modern, compiler.

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

So this is really just up to how well the JIT compiler is able to optimize a particular piece of code, and doesn't necessarily have a logical reason to it.

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

回答by paulsm4

I got similar results:

我得到了类似的结果:

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

I got the SAMEresults if both loops were in the same program, or each was in a separate .java file/.class, executed on a separate run.

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

Finally, here is a javap -c -v <.java>decompile of each:

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

     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)

回答by DSchmidt

Byte codes: https://cs.nyu.edu/courses/fall00/V22.0201-001/jvm2.htmlByte codes Viewer: https://github.com/Konloch/bytecode-viewer

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

On my JDK (Windows 10 64 bit, 1.8.0_65-b17) I can reproduce and explain:

在我的 JDK(Windows 10 64 位,1.8.0_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;
}

Output:

输出:

...
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

So why? The byte code is this:

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

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

The difference being: With brackets (2 * (i * i)):

区别在于: 带括号 ( 2 * (i * i)):

  • push const stack
  • push local on stack
  • push local on stack
  • multiply top of stack
  • multiply top of stack
  • 压入常量栈
  • 将本地推入堆栈
  • 将本地推入堆栈
  • 乘以栈顶
  • 乘以栈顶

Without brackets (2 * i * i):

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

  • push const stack
  • push local on stack
  • multiply top of stack
  • push local on stack
  • multiply top of stack
  • 压入常量栈
  • 将本地推入堆栈
  • 乘以栈顶
  • 将本地推入堆栈
  • 乘以栈顶

Loading all on the stack and then working back down is faster than switching between putting on the stack and operating on it.

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

回答by DSchmidt

When the multiplication is 2 * (i * i), the JVM is able to factor out the multiplication by 2from the loop, resulting in this equivalent but more efficient code:

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

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

but when the multiplication is (2 * i) * i, the JVM doesn't optimize it since the multiplication by a constant is no longer right before the addition.

但是当乘法为 时(2 * i) * i,JVM 不会对其进行优化,因为乘以常数不再正好在加法之前。

Here are a few reasons why I think this is the case:

以下是我认为情况如此的几个原因:

  • Adding an if (n == 0) n = 1statement at the start of the loop results in both versions being as efficient, since factoring out the multiplication no longer guarantees that the result will be the same
  • The optimized version (by factoring out the multiplication by 2) is exactly as fast as the 2 * (i * i)version
  • if (n == 0) n = 1在循环开始处添加语句会导致两个版本的效率相同,因为分解乘法不再保证结果相同
  • 优化后的版本(通过分解乘以 2)与2 * (i * i)版本完全一样快

Here is the test code that I used to draw these conclusions:

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

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

And here are the results:

结果如下:

Fast version: 5.7274411 s
Slow version: 7.6190804 s
Optimized version: 5.1348007 s
Modified fast version: 7.1492705 s
Modified slow version: 7.2952668 s

回答by NoDataFound

I tried a JMH using the default archetype: I also added an optimized version based on Runemoro's explanation.

我尝试使用默认原型的 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;

  @Benchmark
  public int two_square_i() {
    int n = 0;
    for (int i = 0; i < size; i++) {
      n += 2 * (i * i);
    }
    return n;
  }

  @Benchmark
  public int square_i_two() {
    int n = 0;
    for (int i = 0; i < size; i++) {
      n += i * i;
    }
    return 2*n;
  }

  @Benchmark
  public int two_i_() {
    int n = 0;
    for (int i = 0; i < size; i++) {
      n += 2 * i * i;
    }
    return n;
  }
}

The result are here:

结果在这里:

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

On my PC (Core i7860 - it is doing nothing much apart from reading on my smartphone):

在我的 PC 上(Core i7860 - 除了在我的智能手机上阅读之外,它什么也没做):

  • n += i*ithen n*2is first
  • 2 * (i * i)is second.
  • n += i*i然后n*2是第一个
  • 2 * (i * i)第二。

The JVM is clearly not optimizing the same way than a human does (based on Runemoro's answer).

JVM 的优化方式显然与人类不同(基于 Runemoro 的回答)。

Now then, reading bytecode: javap -c -v ./target/classes/org/sample/MyBenchmark.class

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

I am not expert on bytecode, but we iload_2before we imul: that's probably where you get the difference: I can suppose that the JVM optimize reading itwice (iis already here, and there is no need to load it again) whilst in the 2*i*iit can't.

我不是字节码方面的专家,但我们iload_2在我们之前imul:这可能是你得到不同之处:我可以假设 JVM 优化读取i两次(i已经在这里,并且不需要再次加载它)而在2*i*i它可以'吨。

回答by Puzzled

Kasperdasked in a comment of the accepted answer:

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

The Java and C examples use quite different register names. Are both example using the AMD64 ISA?

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

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

I don't have enough reputation to answer this in the comments, but these are the same ISA. It's worth pointing out that the GCC version uses 32-bit integer logic and the JVM compiled version uses 64-bit integer logic internally.

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

R8 to R15 are just new X86_64 registers. EAX to EDX are the lower parts of the RAX to RDX general purpose registers. The important part in the answer is that the GCC version is not unrolled. It simply executes one round of the loop per actual machine code loop. While the JVM version has 16 rounds of the loop in one physical loop (based on rustyx answer, I did not reinterpret the assembly). This is one of the reasons why there are more registers being used since the loop body is actually 16 times longer.

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

回答by ünsal Ers?z

While not directly related to the question's environment, just for the curiosity, I did the same test on .NET Core 2.1, x64, release mode.

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

Here is the interesting result, confirming similar phonomena (other way around) happening over the dark side of the force. Code:

这是一个有趣的结果,证实了在力的阴暗面发生了类似的现象(其他方式)。代码:

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

Result:

结果:

2 * (i * i)

2 * (我 * 我)

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

2 * i * i

2 * 我 * 我

  • result:119860736, 417 ms
  • result:119860736, 417 ms
  • result:119860736, 417 ms
  • result:119860736, 418 ms
  • result:119860736, 418 ms
  • result:119860736, 417 ms
  • result:119860736, 418 ms
  • result:119860736, 416 ms
  • result:119860736, 417 ms
  • result:119860736, 418 ms
  • 结果:119860736,417 毫秒
  • 结果:119860736,417 毫秒
  • 结果:119860736,417 毫秒
  • 结果:119860736,418 毫秒
  • 结果:119860736,418 毫秒
  • 结果:119860736,417 毫秒
  • 结果:119860736,418 毫秒
  • 结果:119860736,416 毫秒
  • 结果:119860736,417 毫秒
  • 结果:119860736,418 毫秒

回答by GhostCat

More of an addendum. I did repro the experiment using the latest Java 8 JVM from IBM:

更多的是一个附录。我使用 IBM 最新的 Java 8 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)

And this shows very similar results:

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

0.374653912 s
n = 119860736
0.447778698 s
n = 119860736

(second results using 2 * i * i).

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

Interestingly enough, when running on the same machine, but using Oracle Java:

有趣的是,当在同一台机器上运行但使用 Oracle Java 时:

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)

results are on average a bit slower:

结果平均有点慢:

0.414331815 s
n = 119860736
0.491430656 s
n = 119860736

Long story short: even the minor version number of HotSpot matter here, as subtle differences within the JIT implementation can have notable effects.

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

回答by Oleksandr Pyrohov

Interesting observation using Java 11and switching off loop unrolling with the following VM option:

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

-XX:LoopUnrollLimit=0

The loop with the 2 * (i * i)expression results in more compact native code1:

带有2 * (i * i)表达式的循环产生更紧凑的本机代码1

L0001: add    eax,r11d
       inc    r8d
       mov    r11d,r8d
       imul   r11d,r8d
       shl    r11d,1h
       cmp    r8d,r10d
       jl     L0001

in comparison with the 2 * i * iversion:

2 * i * i版本对比:

L0001: add    eax,r11d
       mov    r11d,r8d
       shl    r11d,1h
       add    r11d,2h
       inc    r8d
       imul   r11d,r8d
       cmp    r8d,r10d
       jl     L0001

Java version:

爪哇版:

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

Benchmark results:

基准测试结果:

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

Benchmark source code:

基准源代码:

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

    @Benchmark
    public int slow() {
        int n = 0;
        for (int i = 0; i < size; i++)
            n += 2 * i * i;
        return n;
    }

    @Benchmark
    public int fast() {
        int n = 0;
        for (int i = 0; i < size; i++)
            n += 2 * (i * i);
        return n;
    }
}


1 - VM options used: -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -XX:LoopUnrollLimit=0

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