Главная
Блог разработчиков phpBB
 
+ 17 предустановленных модов
+ SEO-оптимизация форума
+ авторизация через соц. сети
+ защита от спама

Потеря точности из Double во Float либо «Куда пропадали копейки?»

Anna | 4.06.2014 | нет комментариев

Преобразование чисел из одного типа в иной традиционно ведется таким образом, Дабы не утратить лишних чисел, т.е. из меньшего типа к больше емкостному. Но что, если предшествующий разрабочик применял конвертацию из Double во Float и стали исчезать копейки в отчетах?
В статье приводится постижение конвертации плавающих чисел в Java:

    99999999.33333333 -> 100000000.0000000
    98888888.33333333 ->  98888888.0000000
     2974815.78000000 ->   2974815.7500000

Давайте разберемся, к чему приводит такое реформирование и отчего все происходит именно так. чай казалось бы, раз используемые в плане числа далеки от максимальных значений типов float и double, то конвертация его из первого во 2-й не должна повлечь за собой негативных последствий в большинстве случаев.

Всякие мысли отменнее подкреплять определенными примерами, следственно сразу же код, тот, что родился вначале на дрожжах реальных цифр, но потом, под влиянием дискуссии на stackoverflow о сходственной конвертации, он превратился в кое-что больше интригующее.

public class Main {

    static void testDoubleToFloat(double d) {
        float f = (float) d;

        System.out.println();
        System.out.println(String.format("double %.10ft%s", d, Long.toBinaryString(Double.doubleToRawLongBits(d))));
        System.out.println(String.format("float  %.10ft   %s", f, Integer.toBinaryString(Float.floatToRawIntBits(f))));
    }

    public static void main(String[] args) {
        System.out.println(String.format("double: %.10f / %.10f", Double.MIN_VALUE, Double.MAX_VALUE));
        System.out.println(String.format("float: %.10f / %.10f", Float.MIN_VALUE, Float.MAX_VALUE));

        /*
            По умолчанию, вычисления с плавающей точкой ведутся с поддержкой double.
        */
        testDoubleToFloat(99999999.0   1.0 / 3.0); // Добавим периодичности
        testDoubleToFloat(98888888.0   1.0 / 3.0); // Вариант без округления девяток
        testDoubleToFloat(2974815.78);
        testDoubleToFloat(-2974815.78);
    }
}

 

Итог выполнения

double: 0.0000000000 / 179769313486231570000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000.0000000000
float: 0.0000000000 / 340282346638528860000000000000000000000.0000000000

double 99999999.3333333300  100000110010111110101111000001111111101010101010101010101010101
float  100000000.0000000000    1001100101111101011110000100000

double 98888888.3333333300  100000110010111100100111011001011100001010101010101010101010101
float  98888888.0000000000     1001100101111001001110110010111

double 2974815.7800000000   100000101000110101100100010111111100011110101110000101000111101
float  2974815.7500000000      1001010001101011001000101111111

double -2974815.7800000000  1100000101000110101100100010111111100011110101110000101000111101
float  -2974815.7500000000    11001010001101011001000101111111

Идентично для

/opt/jdk1.7/bin/java -version
java version "1.7.0_25"
Java(TM) SE Runtime Environment (build 1.7.0_25-b15)
Java HotSpot(TM) 64-Bit Server VM (build 23.25-b01, mixed mode)

И для

java -version
java version "1.7.0_25"
OpenJDK Runtime Environment (IcedTea 2.3.12) (7u25-2.3.12-4ubuntu3)
OpenJDK 64-Bit Server VM (build 23.7-b01, mixed mode)

 

Метод конвертации

Для демонстрации того, что многообразные конструкции идентично конвертируют double во float, добавим многообразные методы и сравним итоги:

public class Main {

    static void testDoubleToFloat(double d) {
        float f = (float) d;
        Float f2 = new Float(d);
        float f3 = Float.parseFloat(new Double(d).toString());
        float f4 = Float.parseFloat(String.format("%.10f", d));

        System.out.println();
        System.out.println(String.format("double %.10ft%s", d, Long.toBinaryString(Double.doubleToRawLongBits(d))));
        System.out.println(String.format("float  %.10ft   %s", f, Integer.toBinaryString(Float.floatToRawIntBits(f))));
        System.out.println(String.format("Float  %.10ft   %s", f2, Integer.toBinaryString(Float.floatToRawIntBits(f2))));
        System.out.println(String.format("float  %.10ft   %s", f3, Integer.toBinaryString(Float.floatToRawIntBits(f3))));

        System.out.println(String.format("float  %.10ft   %s", f4, Integer.toBinaryString(Float.floatToRawIntBits(f4))));
    }

    public static void main(String[] args) {
        System.out.println(String.format("double: %.10f / %.10f", Double.MIN_VALUE, Double.MAX_VALUE));
        System.out.println(String.format("float: %.10f / %.10f", Float.MIN_VALUE, Float.MAX_VALUE));

        testDoubleToFloat(99999999.0   1.0 / 3.0);
        testDoubleToFloat(98888888.0   1.0 / 3.0);
        testDoubleToFloat(2974815.78);
        testDoubleToFloat(-2974815.78);
    }
}

 

Итог выполнения

    double: 0.0000000000 / 179769313486231570000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000.0000000000
    float: 0.0000000000 / 340282346638528860000000000000000000000.0000000000

    double 99999999.3333333300  100000110010111110101111000001111111101010101010101010101010101
    float  100000000.0000000000    1001100101111101011110000100000
    Float  100000000.0000000000    1001100101111101011110000100000
    float  100000000.0000000000    1001100101111101011110000100000
    float  100000000.0000000000    1001100101111101011110000100000

    double 98888888.3333333300  100000110010111100100111011001011100001010101010101010101010101
    float  98888888.0000000000     1001100101111001001110110010111
    Float  98888888.0000000000     1001100101111001001110110010111
    float  98888888.0000000000     1001100101111001001110110010111
    float  98888888.0000000000     1001100101111001001110110010111

    double 2974815.7800000000   100000101000110101100100010111111100011110101110000101000111101
    float  2974815.7500000000      1001010001101011001000101111111
    Float  2974815.7500000000      1001010001101011001000101111111
    float  2974815.7500000000      1001010001101011001000101111111
    float  2974815.7500000000      1001010001101011001000101111111

    double -2974815.7800000000  1100000101000110101100100010111111100011110101110000101000111101
    float  -2974815.7500000000    11001010001101011001000101111111
    Float  -2974815.7500000000    11001010001101011001000101111111
    float  -2974815.7500000000    11001010001101011001000101111111
    float  -2974815.7500000000    11001010001101011001000101111111

Как дозволено удостовериться, выражения «new Float(d)» и “(float) d” дают идентичный итог, т.к. первое использует второе:

/*
 * Copyright (c) 1994, 2010, Oracle and/or its affiliates. All rights reserved.
 * ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
 */
...
public final class Float extends Number implements Comparable<Float> {
...
    public Float(double value) {
        this.value = (float)value;
    }
...
}

Если разбираться с функцией Float.parseFloat, то она отсылает нас через несколько других функций до дальнейшей строки:

    return (float)Double.longBitsToDouble( lbits );

Которая верно таким же образом конвертирует double-переменную во float.
Таким образом, мы удостоверились, что, по крайней мере, в openjdk особенно явственные пути реформирования double во float сводятся к одной конструкции:

float f = (float) d;

 

Форматы хранения float и double

В всяком примере мы вызывали функции Long.toBinaryString для Double и Integer.toBinaryString для Float, Дабы продемонстрировать форматы низкоуровневого хранения сделанных переменных. Красивая статья об этом теснее была написана (Что необходимо знать про арифметику с плавающей запятой, которая стала хорошим переводом английской вики, где отлично рассказано и про двойную точность), следственно тут мы разглядим только то, касается округления.
Представленные выше программы вернули следующие итоги:

    double 2974815.7800000000   100000101000110101100100010111111100011110101110000101000111101
    float  2974815.7500000000      1001010001101011001000101111111

Тип double занимает 64 бита, а тип float 32, но мы видим 63 и 31 знак — это издержка реализации итога, тот, что заканчивается, когда остаются только нули. Следственно, эти числа обязаны выглядеть так:

    double 2974815.7800000000   0 10000010100 0110101100100010111111100011110101110000101000111101
    float  2974815.7500000000      0 10010100 01101011001000101111111

1-й бит — это знак числа. Дальше 11 (для double) либо 8 бит (для float) — экспонента. Позже — мантисса, которая и играет самую увлекательную роль. Первое ощущение: легко теряются все числа позже 23-его бита при конвертации из double во float. Но давайте вначале испробуем восстановить эти числа, Дабы разобраться во каждому по порядку:

  • 1-ые биты 0 => знаки правильные
  • Дальше экспоненты: 0100000101002-102310=21

    и 100101002-12710=21

  • Мантиссы: 1.01101011001000101111111000111101011100001010001111012*221 — 52=2974815.779999749

    и 1.011010110010001011111112*221 — 23=2974815.75

Таким образом, отсекая от двоичного 0110 1011 0010 0010 1111 1110 0011 1101 0111 0000 1010 0011 1101 из double последние 29 цифр, мыполучаем 011 0101 1001 0001 0111 1111 во float, которое дает нам несколько другое число.

Итог

Таким образом, конвертация из формата большей точности может привести к нетривиальным потерям достоверности используемых чисел. Что же касается непринужденно Java, то в ней отменнее применять тип Double, т.к. работа с Float чревата конвертациями из Double с потерями. В своей же системе, мы легко перешли на глобальное применение типа Double, как только выявили факты сходственных конвертаций, тем больше, что сервера давным-давно теснее x64.

P.S.

В системе отслеживания ошибок плана OpenJDK нашлись плотно связанные с данной темой:

В качетве обхода для этих задач написано:

CUSTOMER SUBMITTED WORKAROUND:
Avoiding direct String-to-float conversion by using intermediate doubles.

Источник: programmingmaster.ru

Оставить комментарий
Форум phpBB, русская поддержка форума phpBB
Рейтинг@Mail.ru 2008 - 2017 © BB3x.ru - русская поддержка форума phpBB