这篇文章讲到了绝对值计算的问题:One does not simply calculate the absolute value。
IEEE 754
三个特殊值:
- 如果指数是0并且尾数的小数部分是0,这个数 ±0(和符号位相关)
- 如果指数 = 2^e - 1 并且尾数的小数部分是 0,这个数是 ±∞(同样和符号位相关)
- 如果指数 = 2^e - 1 并且尾数的小数部分非 0,这个数表示为非数(NaN)。
abs 的实现
class Test {
public static double abs(double value) {
if (value < 0) {
return -value;
}
return value;
}
public static void main(String[] args) {
double x = -0.0;
if (1 / abs(x) < 0) {
System.out.println("oops");
}
}
}
if
中加上条件:value == -0.0
是行不通的,因为 +0.0 == -0.0
,可以使用 JDK 中的 Double.compare
:
public static double abs(double value) {
if (value < 0 || Double.compare(value, -0.0) == 0) {
return -value;
}
return value;
}
这样确实有效,不过效率上可能会受到影响,abs
的复杂性就上了一个台阶。
JDK 17 中的实现
public static int compare(double d1, double d2) {
if (d1 < d2)
return -1; // Neither val is NaN, thisVal is smaller
if (d1 > d2)
return 1; // Neither val is NaN, thisVal is larger
// Cannot use doubleToRawLongBits because of possibility of NaNs.
long thisBits = Double.doubleToLongBits(d1);
long anotherBits = Double.doubleToLongBits(d2);
return (thisBits == anotherBits ? 0 : // Values are equal
(thisBits < anotherBits ? -1 : // (-0.0, 0.0) or (!NaN, NaN)
1)); // (0.0, -0.0) or (NaN, !NaN)
}
重新实现
参考 JDK 中的实现,重写 abs
:
private static final long MINUS_ZERO_LONG_BITS = Double.doubleToLongBits(-0.0);
public static double abs(double value) {
if (value < 0 || Double.doubleToLongBits(value) == MINUS_ZERO_LONG_BITS) {
return -value;
}
return value;
}
新的问题:NaN 的处理,处理方法:把 doubleToLongBits
改成 doubleToRawLongBits
。
private static final long MINUS_ZERO_LONG_BITS = Double.doubleToRawLongBits(-0.0);
public static double abs(double value) {
if (value < 0 || Double.doubleToRawLongBits(value) == MINUS_ZERO_LONG_BITS) {
return -value;
}
return value;
}
JVM 的 JIT 会替换这次调用为底层的 CPU 寄存器操作,效率非常可观。
PS:如果可以省去这个分支的判断逻辑,JVM 可以给我们更好的性能优化?
- 中间涉及 CPU 分支预测(branch predictor),如果预测错误,可能会付出相对昂贵的代码。
We know that branches are bad. If the CPU branch predictor guesses incorrectly, they can be very expensive.
- 有传言说,这个调用(
doubleToRawLongBits
)会导致浮点数寄存器转换到通用集成器。Although there are rumors saying that this call may still lead to a transfer from a floating-point register to a general-purpose register. Still it's very fast.
进一步优化
采用 0 减负数等于正数,并且 0 - -0 = 0
的规则:
public static double abs(double value) {
if (value <= 0) {
return 0.0 - value;
}
return value;
}
这就是长期以来(直到最新的 Java 17),JDK 使用的方法(return (a <= 0.0D) ? 0.0D - a : a;
)。
参考:JDK 17 中的的实现:java/lang/Math.java
再进一步
有人提出了意见,认为目前官方的实现 too slow(6506405: Math.abs(float) is slow #4711)。
这就是 jdk-18+6 中引入的新方案(java/lang/Math.java#L1600~L1604):
public static double abs(double a) {
return Double.longBitsToDouble(Double.doubleToRawLongBits(a) & DoubleConsts.MAG_BIT_MASK);
}
DoubleConsts.MAG_BIT_MASK
就是 0x7fffffffffffffffL
, 0 + 63 个 1。
原理就是,通过位运算,清除符号位(使之为 0)。