在所有极端情况下都做到这一点有点棘手。如果我必须解决这样的任务,我通常会从一个天真的实现开始,我可以肯定它是正确的,然后才开始实现优化版本。这样做时,我总是可以与天真的方法进行比较来验证我的结果。
天真的方法是从 1 开始,然后用 / 除以 2,直到我们将输入的绝对值括起来。然后,我们将输出较近的边界。它实际上有点复杂:如果该值是 NaN 或无穷大,则需要特殊处理。
代码如下:
public static double getClosestPowerOf2Loop(final double x) {
final double absx = Math.abs(x);
double prev = 1.0;
double next = 1.0;
if (Double.isInfinite(x) || Double.isNaN(x)) {
return x;
} else if (absx < 1.0) {
do {
prev = next;
next /= 2.0;
} while (next > absx);
} else if (absx > 1.0) {
do {
prev = next;
next *= 2.0;
} while (next < absx);
}
if (x < 0.0) {
prev = -prev;
next = -next;
}
return (Math.abs(next - x) < Math.abs(prev - x)) ? next : prev;
}
我希望代码清晰,无需进一步解释。从 Java 8 开始,您可以使用 !Double.isFinite(x) 代替 Double.isInfinite(x) || Double.isNaN(x)。
让我们看看优化版本。正如其他答案已经建议的那样,我们可能应该查看位表示。 Java 要求使用 IEE 754 表示浮点值。在该格式中,double(64 位)精度的数字表示为
我们将再次特例 NaN 和无穷大(由特殊位模式表示)。但是,还有另一个例外:尾数的最高有效位是隐式 1,并且在位模式中找不到 - 除了非常小的数字,所谓的次正常 表示我们使用的最高有效位不是尾数的最高有效位。因此,对于普通数,我们将简单地将尾数的位设置为全 0,但对于次正规数,我们将其转换为一个数字,其中只保留最高有效位 1 位。这个过程总是向零舍入,所以要得到另一个界限,我们只需乘以 2。
让我们看看这一切是如何协同工作的:
public static double getClosestPowerOf2Bits(final double x) {
if (Double.isInfinite(x) || Double.isNaN(x)) {
return x;
} else {
final long bits = Double.doubleToLongBits(x);
final long signexp = bits & 0xfff0000000000000L;
final long mantissa = bits & 0x000fffffffffffffL;
final long mantissaPrev = Math.abs(x) < Double.MIN_NORMAL
? Long.highestOneBit(mantissa)
: 0x0000000000000000L;
final double prev = Double.longBitsToDouble(signexp | mantissaPrev);
final double next = 2.0 * prev;
return (Math.abs(next - x) < Math.abs(prev - x)) ? next : prev;
}
}
我完全确定我已经涵盖了所有极端情况,但确实会运行以下测试:
public static void main(final String[] args) {
final double[] values = {
5.0, 4.1, 3.9, 1.0, 0.0, -0.1, -8.0, -8.1, -7.9,
0.9 * Double.MIN_NORMAL, -0.9 * Double.MIN_NORMAL,
Double.NaN, Double.MAX_VALUE, Double.MIN_VALUE,
Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY,
};
for (final double value : values) {
final double powerL = getClosestPowerOf2Loop(value);
final double powerB = getClosestPowerOf2Bits(value);
System.out.printf("%17.10g --> %17.10g %17.10g%n",
value, powerL, powerB);
assert Double.doubleToLongBits(powerL) == Double.doubleToLongBits(powerB);
}
}
输出:
5.000000000 --> 4.000000000 4.000000000
4.100000000 --> 4.000000000 4.000000000
3.900000000 --> 4.000000000 4.000000000
1.000000000 --> 1.000000000 1.000000000
0.000000000 --> 0.000000000 0.000000000
-0.1000000000 --> -0.1250000000 -0.1250000000
-8.000000000 --> -8.000000000 -8.000000000
-8.100000000 --> -8.000000000 -8.000000000
-7.900000000 --> -8.000000000 -8.000000000
2.002566473e-308 --> 2.225073859e-308 2.225073859e-308
-2.002566473e-308 --> -2.225073859e-308 -2.225073859e-308
NaN --> NaN NaN
1.797693135e+308 --> 8.988465674e+307 8.988465674e+307
4.900000000e-324 --> 4.900000000e-324 4.900000000e-324
-Infinity --> -Infinity -Infinity
Infinity --> Infinity Infinity
性能怎么样?
我已经运行了以下基准测试
public static void main(final String[] args) {
final Random rand = new Random();
for (int i = 0; i < 1000000; ++i) {
final double value = Double.longBitsToDouble(rand.nextLong());
final double power = getClosestPowerOf2(value);
}
}
其中getClosestPowerOf2 将替换为getClosestPowerOf2Loop 或getClosestPowerOf2Bits。在我的笔记本电脑上,我得到以下结果:
-
getClosestPowerOf2Loop:2.35 秒
-
getClosestPowerOf2Bits:1.80 秒
这真的值得吗?