首先,让我们使用JMH重新编写基准测试,以避免常见的基准测试陷阱。
public class FloatCompare {
@Benchmark
public float cmp() {
float num = ThreadLocalRandom.current().nextFloat() * 2 - 1;
return num < 0 ? 0 : num;
}
@Benchmark
public float mul() {
float num = ThreadLocalRandom.current().nextFloat() * 2 - 1;
return num * (num < 0 ? 0 : 1);
}
}
JMH 还建议使用乘法代码可以更快:
Benchmark Mode Cnt Score Error Units
FloatCompare.cmp avgt 5 12,940 ± 0,166 ns/op
FloatCompare.mul avgt 5 6,182 ± 0,101 ns/op
现在是时候使用内置于JMH中的perfasm性能分析器来查看JIT编译器生成的汇编代码。以下是输出的最重要部分(注释是我的):
cmp
方法:
5,65% │││ 0x0000000002e717d0: vxorps xmm1,xmm1,xmm1 ; xmm1 := 0
0,28% │││ 0x0000000002e717d4: vucomiss xmm1,xmm0 ; compare num < 0 ?
4,25% │╰│ 0x0000000002e717d8: jbe 2e71720h ; jump if num >= 0
9,77% │ ╰ 0x0000000002e717de: jmp 2e71711h ; jump if num < 0
mul
方法:
1,59% ││ 0x000000000321f90c: vxorps xmm1,xmm1,xmm1
3,80% ││ 0x000000000321f910: mov r11d,1h
││ 0x000000000321f916: xor r8d,r8d
││ 0x000000000321f919: vucomiss xmm1,xmm0
2,23% ││ 0x000000000321f91d: cmovnbe r11d,r8d
5,06% ││ 0x000000000321f921: vcvtsi2ss xmm1,xmm1,r11d
7,04% ││ 0x000000000321f926: vmulss xmm0,xmm1,xmm0
关键区别在于mul
方法中没有跳转指令。相反,使用了条件移动指令cmovnbe
。
cmov
使用整数寄存器。由于(num < 0 ? 0 : 1)
表达式在右侧使用整数常量,JIT足够聪明,会发出条件移动指令而不是条件跳转指令。
在这个基准测试中,条件跳转非常低效,因为由于数字的随机性质,分支预测经常失败。这就是为什么mul
方法的无分支代码看起来更快的原因。
如果我们以某种方式修改基准测试,使一个分支优于另一个分支,例如通过替换
ThreadLocalRandom.current().nextFloat() * 2 - 1
with
ThreadLocalRandom.current().nextFloat() * 2 - 0.1f
那么分支预测就会更好地工作,cmp
方法的速度将变得和 mul
一样快:
Benchmark Mode Cnt Score Error Units
FloatCompare.cmp avgt 5 5,793 ± 0,045 ns/op
FloatCompare.mul avgt 5 5,764 ± 0,048 ns/op
num * (num < 0 ? 0 : 1);
中的一个结果整数更改为float
,例如0f
或1f
时,该语句变得和其他语句一样慢。 - akuzminykh