【发布时间】:2020-01-18 06:59:48
【问题描述】:
如您所知,Math.abs(Integer.MIN_VALUE) == Integer.MIN_VALUE 为防止出现负值,我的项目中实现了safeAbs 方法:
public static int safeAbs(int i) {
i = Math.abs(i);
return i < 0 ? 0 : i;
}
我将性能与以下性能进行了比较:
public static int safeAbs(int i) {
return i == Integer.MIN_VALUE ? 0 : Math.abs(i);
}
第一个几乎比第二个慢 6 倍(第二个性能几乎与“纯”Math.abs(int) 相同)。从我的角度来看,字节码没有显着差异,但我猜JIT“汇编”代码中存在差异:
“慢”版本:
0x00007f0149119720: mov %eax,0xfffffffffffec000(%rsp)
0x00007f0149119727: push %rbp
0x00007f0149119728: sub $0x20,%rsp
0x00007f014911972c: test %esi,%esi
0x00007f014911972e: jl 0x7f0149119734
0x00007f0149119730: mov %esi,%eax
0x00007f0149119732: jmp 0x7f014911973c
0x00007f0149119734: neg %esi
0x00007f0149119736: test %esi,%esi
0x00007f0149119738: jl 0x7f0149119748
0x00007f014911973a: mov %esi,%eax
0x00007f014911973c: add $0x20,%rsp
0x00007f0149119740: pop %rbp
0x00007f0149119741: test %eax,0x1772e8b9(%rip) ; {poll_return}
0x00007f0149119747: retq
0x00007f0149119748: mov %esi,(%rsp)
0x00007f014911974b: mov $0xffffff65,%esi
0x00007f0149119750: nop
0x00007f0149119753: callq 0x7f01490051a0 ; OopMap{off=56}
;*ifge
; - math.FastAbs::safeAbsSlow@6 (line 16)
; {runtime_call}
0x00007f0149119758: callq 0x7f015f521d20 ; {runtime_call}
“正常”版本:
# {method} {0x00007f31acf28cd8} 'safeAbsFast' '(I)I' in 'math/FastAbs'
# parm0: rsi = int
# [sp+0x30] (sp of caller)
0x00007f31b08c7360: mov %eax,0xfffffffffffec000(%rsp)
0x00007f31b08c7367: push %rbp
0x00007f31b08c7368: sub $0x20,%rsp
0x00007f31b08c736c: cmp $0x80000000,%esi
0x00007f31b08c7372: je 0x7f31b08c738e
0x00007f31b08c7374: mov %esi,%r10d
0x00007f31b08c7377: neg %r10d
0x00007f31b08c737a: test %esi,%esi
0x00007f31b08c737c: mov %esi,%eax
0x00007f31b08c737e: cmovl %r10d,%eax
0x00007f31b08c7382: add $0x20,%rsp
0x00007f31b08c7386: pop %rbp
0x00007f31b08c7387: test %eax,0x162c2c73(%rip) ; {poll_return}
0x00007f31b08c738d: retq
0x00007f31b08c738e: mov %esi,(%rsp)
0x00007f31b08c7391: mov $0xffffff65,%esi
0x00007f31b08c7396: nop
0x00007f31b08c7397: callq 0x7f31b07b11a0 ; OopMap{off=60}
;*if_icmpne
; - math.FastAbs::safeAbsFast@3 (line 17)
; {runtime_call}
0x00007f31b08c739c: callq 0x7f31c5863d20 ; {runtime_call}
基准代码:
@BenchmarkMode(Mode.AverageTime)
@Fork(value = 1, jvmArgsAppend = {"-Xms3g", "-Xmx3g", "-server"})
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@State(Scope.Benchmark)
@Threads(1)
@Warmup(iterations = 10)
@Measurement(iterations = 10)
public class SafeAbsMicroBench {
@State(Scope.Benchmark)
public static class Data {
final int len = 10_000_000;
final int[] values = new int[len];
@Setup(Level.Trial)
public void setup() {
// preparing 10 million random integers without MIN_VALUE
for (int i = 0; i < len; i++) {
int val;
do {
val = ThreadLocalRandom.current().nextInt();
} while (val == Integer.MIN_VALUE);
values[i] = val;
}
}
}
@Benchmark
public int safeAbsSlow(Data data) {
int sum = 0;
for (int i = 0; i < data.len; i++)
sum += safeAbsSlow(data.values[i]);
return sum;
}
@Benchmark
public int safeAbsFast(Data data) {
int sum = 0;
for (int i = 0; i < data.len; i++)
sum += safeAbsFast(data.values[i]);
return sum;
}
private int safeAbsSlow(int i) {
i = Math.abs(i);
return i < 0 ? 0 : i;
}
private int safeAbsFast(int i) {
return i == Integer.MIN_VALUE ? 0 : Math.abs(i);
}
public static void main(String[] args) throws RunnerException {
final Options options = new OptionsBuilder()
.include(SafeAbsMicroBench.class.getSimpleName())
.build();
new Runner(options).run();
}
}
结果(Linux x86-64、7820HQ,在 oracle jdk 8 和 11 上检查,结果非常相似)。
Benchmark Mode Cnt Score Error Units
SafeAbsMicroBench.safeAbsFast avgt 10 6435155.516 ± 47130.767 ns/op
SafeAbsMicroBench.safeAbsSlow avgt 10 35646411.744 ± 776173.621 ns/op
谁能解释为什么第一个代码比第二个慢很多?
【问题讨论】:
-
“正常”版本在“慢”版本分支的地方有一个 cmov,但它们在返回下方都有一些神秘的分支,这是什么意思?
-
谢谢你指点CMOV,但我不完全明白你在问什么,我不是汇编专家,为了简单起见,我只复制了编译方法中看起来不同的那些部分- 如有必要,我可以扩展它,但在我看来,每个人都可以自己“打印汇编”。
-
不相关,但
Integer.MAX_VALUE不是abs(Integer.MIN_VALUE)的更好替代品吗?当然,它已经关闭了 1,但这仍然比关闭 2147483648 更好...... -
@tobias_k well 0 是您可以获得的最小绝对值。这在语义上 0 似乎是不错的选择...
-
通过查看代码和程序集,“天真的”答案是: 1. 在“慢”的情况下,
abs函数被调用总是,该程序集包含“更多”跳转指令(用于abs本身和随后的比较)。 2. 在“快速”的情况下,只有 一个 跳转,这会检查一个准确的值,并且这个跳转在测试中从不(!)执行(yadda -yadda-branch-prediction-maybe...?)。所以在快速的情况下,它只是遍历一个指令列表,而在慢速的情况下,它必须跳得更多。在测试中添加 500 万MIN_VALUEs 可能会很有趣...
标签: java performance x86-64 jit