Ближайшее кратное степени двойки дроби

Существует ли оптимизированный и эффективный способ округления числа double до точного значения, ближайшего кратного заданной степени двойки?

Другими словами, округление .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

person durron597    schedule 30.01.2015    source источник
comment
Как вы ожидаете, что сохранение вашего результата будет отличаться от обычного двойного значения? Все двойные числа кратны 2 ^ x просто из-за экспоненциальной части двойного числа. Что вы надеетесь получить? Вы хотите, чтобы ваш результат точно представлял значение 2^x?   -  person midor    schedule 30.01.2015
comment
Лучшим вариантом здесь было бы пройти длинную маску, а затем снова преобразовать обратно в двойную.   -  person fge    schedule 30.01.2015
comment
@fge Я согласен, но я не совсем уверен, как это сделать. Особенно, если целая часть дроби не равна нулю, например. 12345.4375   -  person durron597    schedule 30.01.2015
comment
Как это проблема? Вы меняете только дробь, а не показатель   -  person fge    schedule 30.01.2015
comment
@fge Потому что вам нужно маскировать меньше битов, когда показатель степени больше   -  person durron597    schedule 30.01.2015


Ответы (5)


Если вы хотите выбрать степень двойки, самый простой способ — умножить, например, на 16, округлите до ближайшего целого, затем разделите на 16. Обратите внимание, что деление на степень двойки является точным, если результатом является нормальное число. Это может вызвать ошибку округления для субнормальных чисел.

Вот пример программы, использующей эту технику:

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
person Patricia Shanahan    schedule 30.01.2015
comment
Первый абзац этого ответа является прямым ответом на неясную формулировку моего исходного вопроса. С тех пор я исправил вопрос, чтобы уточнить. - person durron597; 30.01.2015
comment
@ durron597 Durron597 Я соответствующим образом отредактировал свой ответ и добавил пример программы. - person Patricia Shanahan; 30.01.2015
comment
Я проголосовал за вас. Интересно, быстрее ли этот ответ, чем мой ответ с битовой маской, а также работает ли он для больших целых частей. - person durron597; 30.01.2015
comment
@durron597 Durron597 Хорошее замечание по поводу большого значения - оно потерпит неудачу, если результат умножения переполнится long. - person Patricia Shanahan; 30.01.2015
comment
Я исправил проблему с переполнением @durron597, используя Math.rint вместо Math.round. Теперь он будет переполняться только в том случае, если результат умножения больше Double.MAX_VALUE. - person Patricia Shanahan; 30.01.2015
comment
Я провел сравнение вашего ответа и моего ответа с помощью штангенциркуля Google, и этот ответ значительно быстрее. Иметь галочку. - person durron597; 30.01.2015
comment
Давайте продолжим обсуждение в чате. - person Patricia Shanahan; 30.01.2015

Если вопрос касается округления любого числа до заранее определенной двоичной точности, вам нужно сделать следующее:

  1. Преобразуйте значение в long, используя 'Double .doubleToLongBits()`
  2. Изучите экспоненту: если она слишком велика (exponent+required precision>51, количество битов в мантиссе), вы не сможете выполнить какое-либо округление, но вам и не придется этого делать: число уже удовлетворяет вашим критериям.
  3. Если, с другой стороны, exponent+required precision<0, результат округления всегда равен 0.
  4. В любом другом случае посмотрите на мантиссу и вычеркните все биты, которые находятся ниже exponent+required precision-го значащего бита.
  5. Преобразуйте число обратно в двойное, используя Double.longBitsToDouble()
person biziclop    schedule 30.01.2015
comment
Я попытался реализовать этот ответ - person durron597; 30.01.2015

Сделать это правильно во всех крайних случаях немного сложно. Если мне нужно решить такую ​​задачу, я обычно начинаю с наивной реализации, в которой могу быть уверен, что она будет правильной, и только потом приступаю к реализации оптимизированной версии. При этом я всегда могу сравнить с наивным подходом, чтобы подтвердить свои результаты.

Наивный подход состоит в том, чтобы начать с 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 заменяется на getClosestPowerOf2Loop или getClosestPowerOf2Bits. На моем ноутбуке я получаю следующие результаты:

  • getClosestPowerOf2Loop: 2,35 с
  • getClosestPowerOf2Bits: 1,80 с

Стоило ли это усилий?

person 5gon12eder    schedule 30.01.2015
comment
В моем реальном приложении я знаю, что ввод никогда не будет Infinity, NaN или отрицательным, поэтому мне не нужно быть таким осторожным. - person durron597; 30.01.2015
comment
Кроме того, цикл здесь никогда не нужен, и мой ответ, и ответ Патриции не используют циклы. - person durron597; 30.01.2015
comment
Моя вторая функция также не использует цикл. Я показал циклический подход, чтобы дать простое решение, которое, как я могу быть уверен, правильно покроет все случаи. - person 5gon12eder; 30.01.2015

Вам понадобится немного магии битов, если вы собираетесь округлять до произвольных степеней 2.

Вам нужно будет проверить экспоненту:

int exponent = Math.getExponent(inexact);

Затем, зная, что в мантиссе 53 бита, можно найти бит, на который нужно округлить.


Или просто сделайте:

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

Я использую Math.round, потому что ожидаю, что он будет оптимальным для задачи, а не пытается реализовать его самостоятельно.

person ratchet freak    schedule 30.01.2015

Вот моя первая попытка решения, которое не обрабатывает все случаи в ответе @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;
}
person durron597    schedule 30.01.2015
comment
Он будет округлен до нуля, что является нижним значением для положительного значения, значением для отрицательного значения. Я предлагаю использовать Math.rint для хранения intPart как двойника, а не как int, который будет переполняться для всех, кроме относительно небольших двойников. - person Patricia Shanahan; 30.01.2015