故事是从一个问题开始的:为什么 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*(ii) 始终优于 2i*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_2和imul的顺序不一致所有字节码都是相同的,那么这个顺序为什么会有如此大的区别呢?字节码不行那就更下一层,让我们看看汇编代码的区别。
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