动态

详情 返回 返回

“傻傻”的JAVA編譯器 - 动态 详情

故事是從一個問題開始的:為什麼 Java 中 2 * (i * i) 比 2 * i * i更快?

猛地一看,我還以為有人在釣魚,這倆玩意不應該是一模一樣嗎?第二反應是計算結果溢出了int值所以導致了這個差異,於是我掏出JMH這個利器準備開始一輪驗證,為了避免干擾,構造了不同的測試用例集用於縱向與橫向的比較。

@BenchmarkMode(Mode.AverageTime)                  // 測試方法平均執行時間
@OutputTimeUnit(TimeUnit.NANOSECONDS)             // 輸出結果的時間粒度為納秒
@State(Scope.Thread)                              // 運行相同測試的所有線程將共享實例。可以用來測試狀態對象的多線程性能(或者僅標記該範圍的基準)。
@Warmup(iterations = 2, time = 1)                 // 執行5遍預熱
@Measurement(iterations = 10, time = 1)           // 執行5遍測試
@Fork(1)
public class CompileBenchMarkDemo {

  @Param({"1477", "1000000000"})
  private int size;

  public static void main(String[] args) throws RunnerException {
    Options opt = new OptionsBuilder().include(CompileBenchMarkDemo.class.getSimpleName())
        .build();
    new Runner(opt).run();
  }

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

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

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

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

}

可以看到為了避免int溢出的干擾我使用了long來做累加,還有將i的範圍限制到1477保證int不溢出

openjdk version "1.8.0_382-internal"
OpenJDK Runtime Environment (build 1.8.0_382-internal-b05)
OpenJDK 64-Bit Server VM (build 25.382-b05, mixed mode)

在上述jdk版本下最終結果如下:

Benchmark                                     (size)  Mode  Cnt          Score          Error  Units
CompileBenchMarkDemo.twoSquare            1000000000  avgt   10  461970038.267 ± 13350368.542  ns/op
CompileBenchMarkDemo.twoNoSquare          1000000000  avgt   10  744365243.050 ± 37840802.126  ns/op
CompileBenchMarkDemo.twoSquareWithLong    1000000000  avgt   10  799079820.550 ± 12835540.327  ns/op
CompileBenchMarkDemo.twoNoSquareWithLong  1000000000  avgt   10  906846479.450 ± 41247832.592  ns/op
CompileBenchMarkDemo.twoSquare                  1477  avgt   10        717.674 ±       19.992  ns/op
CompileBenchMarkDemo.twoNoSquare                1477  avgt   10       1144.875 ±      100.242  ns/op
CompileBenchMarkDemo.twoSquareWithLong          1477  avgt   10        979.668 ±       58.368  ns/op
CompileBenchMarkDemo.twoNoSquareWithLong        1477  avgt   10       1208.143 ±       97.710  ns/op

結果出乎我的意料,沒想到在溢出和不溢出的情況下2 * ( i * i ) 始終優於 2 * i * i

於是我打算從字節碼一探究竟

 0 iconst_0
 1 istore_1
 2 iconst_0
 3 istore_2
 4 iload_2
 5 ldc #11 <1000000000>
 7 if_icmpge 24 (+17)
10 iload_1
11 iconst_2
12 iload_2
13 iload_2
14 imul
15 imul
16 iadd
17 istore_1
18 iinc 2 by 1
21 goto 4 (-17)
24 iload_1
25 ireturn
0 iconst_0
1 istore_1
2 iconst_0
3 istore_2
4 iload_2
5 ldc #11 <1000000000>
7 if_icmpge 24 (+17)
10 iload_1
11 iconst_2
12 iload_2
13 imul
14 iload_2
15 imul
16 iadd
17 istore_1
18 iinc 2 by 1
21 goto 4 (-17)
24 iload_1
25 ireturn

可以看到字節碼上除了iload_2imul的順序不一致所有字節碼都是相同的,那麼這個順序為什麼會有如此大的區別呢?字節碼不行那就更下一層,讓我們看看彙編代碼的區別。

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

我們看到有 1 個寄存器”溢出”到堆棧上。

對於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

在這裏,由於需要保留更多的中間結果,我們觀察到更多的“溢出”和對堆棧的更多訪問。

因此,問題的答案很簡單:2 * (i * i)2 * i * i更快,因為 JIT 生成了更優化的彙編代碼。

Java的JIT是個非常有價值的東西,但有的時候它也可能“犯傻”,我們在平時寫代碼的過程中對於這些點倒也無需刻意去記憶,這本該是編譯器自己要做的事情,祝願Java的編譯器越來越好吧。

原文鏈接:https://pebble-skateboard-d46.notion.site/JAVA-a91ca0b1305e49918efcdd0035a7a6e6

參考資料

https://stackoverflow.com/questions/53452713/why-is-2-i-i-faster-than-2-i-i-in-java

user avatar u_16297326 头像 journey_64224c9377fd5 头像 seazhan 头像 soroqer 头像 yizhidanshendetielian 头像 lvweifu 头像 pottercoding 头像 ruozxby 头像 javadog 头像 fengliudelazhu 头像 sorra 头像 jame_5f6d5e99aea15 头像
点赞 16 用户, 点赞了这篇动态!
点赞

Add a new 评论

Some HTML is okay.