两个分数的最近倍数

时间:2015-01-30 14:35:12

标签: java double rounding

是否有一种优化的,高性能的方法可以将double舍入到最接近两个分数的给定幂的倍数的精确值?

换句话说,将.44舍入到最接近的1/16(换句话说,将n/16表示为n,其中.4375是一个整数)将是{ {1}}。注意:这是相关的,因为可以存储两个分数的幂而没有舍入误差,例如

public class PowerOfTwo {
  public static void main(String... args) {
    double inexact = .44;
    double exact = .4375;

    System.out.println(inexact + ":   " + Long.toBinaryString(Double.doubleToLongBits(inexact)));
    System.out.println(exact + ": " + Long.toBinaryString(Double.doubleToLongBits(exact)));
  }
}

输出:

0.44:   11111111011100001010001111010111000010100011110101110000101001
0.4375: 11111111011100000000000000000000000000000000000000000000000000

6 个答案:

答案 0 :(得分:3)

如果你想选择2的幂,最简单的方法是乘以例如16,舍入到最接近的整数,然后除以16.注意,如果结果是正常数,则除以2的幂是精确的。它可能导致次正规数的舍入误差。

以下是使用此技术的示例程序:

public class Test {
  public static void main(String[] args) {
    System.out.println(roundToPowerOfTwo(0.44, 2));
    System.out.println(roundToPowerOfTwo(0.44, 3));
    System.out.println(roundToPowerOfTwo(0.44, 4));
    System.out.println(roundToPowerOfTwo(0.44, 5));
    System.out.println(roundToPowerOfTwo(0.44, 6));
    System.out.println(roundToPowerOfTwo(0.44, 7));
    System.out.println(roundToPowerOfTwo(0.44, 8));
  }

  public static double roundToPowerOfTwo(double in, int power) {
    double multiplier = 1 << power;
    return Math.rint(in * multiplier) / multiplier;
  }
}

输出:

0.5
0.5
0.4375
0.4375
0.4375
0.4375
0.44140625

答案 1 :(得分:1)

如果问题是将任何数字四舍五入为预定的二进制精度,那么您需要做的是:

  1. 使用'Double.doubleToLongBits()`
  2. 将值转换为long
  3. 检查指数:如果它太大(exponent+required precision>51,有效数字中的位数),您将无法进行任何舍入,但您不必:该数字已经满足您的要求标准。
  4. 如果另一方面exponent+required precision<0,则舍入的结果始终为0.
  5. 在任何其他情况下,查看有效数字并清除exponent+required precision重要位下面的所有位。
  6. 使用Double.longBitsToDouble()
  7. 将数字转换回加倍

答案 2 :(得分:1)

在所有角落案件中做到这一点都有点棘手。如果我必须解决这样的任务,我通常会从一个天真的实现开始,我可以非常肯定是正确的,然后才开始实现优化版本。在这样做的同时,我总是可以与天真的方法进行比较来验证我的结果。

天真的方法是从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位)精度的数字表示为

  • 1位标志,
  • 11位指数和
  • 52位尾数。

我们将再次使用特殊情况的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将由getClosestPowerOf2LoopgetClosestPowerOf2Bits替换。在我的笔记本电脑上,我得到以下结果:

  • getClosestPowerOf2Loop:2.35 s
  • getClosestPowerOf2Bits:1.80 s

这真的值得付出努力吗?

答案 3 :(得分:0)

这是我第一次尝试解决方案,它并没有处理@ biziclop的答案中的所有案例,而且可能会在#34; floor&#34;而不是&#34; round&#34;

public static double round(double d, int precision) {
  double longPart = Math.rint(d);
  double decimalOnly = d - longPart;
  long bits = Double.doubleToLongBits(decimalOnly);

  long mask = -1l << (54 - precision);

  return Double.longBitsToDouble(bits & mask) + longPart;
}

答案 4 :(得分:0)

如果要打算使用2的任意幂,你将需要一些魔法。

您需要检查指数:

int exponent = Math.getExponent(inexact);

然后知道尾数中有53位可以找到你需要舍入的位。


或者只是这样做:

Math.round(inexact* (1l<<exponent))/(1l<<exponent)

我使用Math.round是因为我希望它对于任务来说是最佳的,而不是试图自己实现它。

答案 5 :(得分:0)

我看到这篇文章试图解决一个相关问题:如何有效地找到包含任何给定正则实数值的 2 的两个幂。由于我的程序处理除双打之外的许多类型,因此我需要一个通用的解决方案。想要四舍五入到最接近的 2 次方的人可以获得括号值并选择最接近的值。在我的情况下,一般解决方案需要 BigDecimals。这是我使用的技巧。

对于大于 1 的数字:

            int exponent = myBigDecimal.toBigInteger.bitLength() - 1;
            BigDecimal lowerBound = TWO.pow(exponent);
            BigDecimal upperBound = TWO.pow(exponent+1);

对于数字 > 0 和 < 1:

            int exponent = -(BigDecimal.ONE.divide(myBigDecimal, myContext).toBigInteger().bitLength()-1); 
            BigDecimal lowerBound = TWO.pow(exponent-1);
            BigDecimal upperBound = TWO.pow(exponent);

我只列出了正面案例。你通常取一个数字,并在它的绝对值上使用这个算法。然后,如果在原始问题中数字为负,则将算法的结果乘以 -1。最后,原始 num == 0 或 num == 1 在此算法之外处理是微不足道的。这涵盖了整个实数轴,除了您在调用此算法之前处理的 infinties 和 nans 。

相关问题