【问题标题】:Nearest multiple of a power of two fraction两个分数的幂的最近倍数
【发布时间】:2015-01-30 14:35:11
【问题描述】:

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

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

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

【问题讨论】:

  • 您希望存储的结果与正常的双精度值有何不同?由于双精度数的指数部分,所有双精度数都是 2^x 的倍数。你希望得到什么?您是否希望您的结果准确地表示 2^x 的值?
  • 这里最好的办法是通过一个长掩码,然后再次转换回双倍
  • @fge 我同意,但我不确定该怎么做。特别是如果分数的整数部分不为零,例如12345.4375
  • 这是怎么回事?你只改变分数,而不是指数
  • @fge 因为当指数较大时您必须屏蔽更少的位

标签: java double rounding


【解决方案1】:

如果您想选择 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

【讨论】:

  • 此回复的第一段是对我最初问题中措辞不清楚的直接回复。我已经解决了这个问题以澄清。
  • @durron597 我已经相应地编辑了我的答案,并添加了一个示例程序。
  • 我支持你。我想知道这个答案是否比我的位掩码答案更快,以及它是否适用于大整数部分。
  • @durron597 关于大值的好处 - 如果乘法的结果溢出long,它将失败。
  • 我已通过使用Math.rint 而不是Math.round 更正了@durron597 提出的溢出问题。现在只有当乘法的结果大于Double.MAX_VALUE时才会溢出。
【解决方案2】:

如果问题是关于将任何数字四舍五入到预先确定的二进制精度,您需要做的是:

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

【讨论】:

  • 我试图实现这个答案
【解决方案3】:

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

天真的方法是从 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 秒
  • getClosestPowerOf2Bits:1.80 秒

这真的值得吗?

【讨论】:

  • 在我的实际应用程序中,我知道输入永远不会是 Infinity、NaN 或负数,所以我不需要这么小心。
  • 另外,这里从来不需要循环,我的回答和帕特里夏的回答都不使用循环。
  • 我的第二个函数也不使用循环。我已经展示了循环方法来提供一个简单的解决方案,我可以合理地确定它会正确涵盖所有情况。
【解决方案4】:

如果你要四舍五入到 2 的任意幂,你将需要一些魔法。

您需要检查指数:

int exponent = Math.getExponent(inexact);

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


或者干脆做:

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

我使用 Math.round 是因为我希望它最适合任务,而不是尝试自己实现。

【讨论】:

    【解决方案5】:

    这是我第一次尝试解决方案,它不能处理@biziclop 答案中的所有情况,并且可能是“地板”而不是“圆形”

    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;
    }
    

    【讨论】:

    • 它将向零舍入,这是正数的下限,负数是 ceil。我建议使用 Math.rint 将 intPart 存储为双精度数,而不是一个会溢出的整数,除了相对较小的双精度数。
    【解决方案6】:

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

    对于数字 > 1:

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

    对于 > 0 和

                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。

    【讨论】:

    • 如果您正在寻找小数点后最接近的二的幂(就像您正在做的坐标四舍五入),我应该添加您的数字并且只计算分数值(删除整数部分)使用上面的 > 0 和
    • 这实际上比公认的答案更有效吗?
    • @durron597 我认为不会。这是一个通用的解决方案,可以适应其他字节大小等。任何人都可以根据自己的需要专门处理它。我把它作为一个指针,供人们寻找类似问题的解决方案。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2013-09-03
    • 2021-06-23
    • 2011-03-25
    • 1970-01-01
    • 2015-01-13
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多