Преобразование десятичного числа в двойное число в С# приводит к различию

Краткое описание проблемы:

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

Что еще хуже, так это то, что при преобразовании могут быть два "равных" десятичных значения.

Пример кода:

decimal dcm = 8224055000.0000000000m;  // dcm = 8224055000
double dbl = Convert.ToDouble(dcm);    // dbl = 8224055000.000001

decimal dcm2 = Convert.ToDecimal(dbl); // dcm2 = 8224055000
double dbl2 = Convert.ToDouble(dcm2);  // dbl2 = 8224055000.0

decimal deltaDcm = dcm2 - dcm;         // deltaDcm = 0
double deltaDbl = dbl2 - dbl;          // deltaDbl = -0.00000095367431640625

Посмотрите на результаты в комментариях. Результаты копируются из часов отладчика. Числа, которые производят этот эффект, имеют гораздо меньше десятичных цифр, чем предел типов данных, поэтому он не может быть переполнением (я думаю!).

Интересно то, что может быть два десятичных значения equal (в примере кода выше, см. "dcm" и "dcm2", при этом "deltaDcm" равен нулю), что приводит к в разные двойные значения при преобразовании. (В коде "dbl" и "dbl2", которые имеют ненулевой "deltaDbl" )

Я предполагаю, что это должно быть связано с различием в поразрядном представлении чисел в двух типах данных, но не может понять, что! И мне нужно знать, что делать, чтобы преобразовать, как мне это нужно. (например, dcm2 → dbl2)

Ответ 1

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

Здесь немного более простая демонстрация, используя DoubleConverter.cs, который я использовал несколько раз раньше.

using System;

class Test
{
    static void Main()
    {
        decimal dcm1 = 8224055000.0000000000m;
        decimal dcm2 = 8224055000m;
        double dbl1 = (double) dcm1;
        double dbl2 = (double) dcm2;

        Console.WriteLine(DoubleConverter.ToExactString(dbl1));
        Console.WriteLine(DoubleConverter.ToExactString(dbl2));
    }
}

Результаты:

8224055000.00000095367431640625
8224055000

Теперь возникает вопрос, почему исходное значение (8224055000.0000000000), которое является целым числом, и точно представляемое как double, заканчивается дополнительными данными. Я сильно подозреваю это из-за причуд в алгоритме, используемом для преобразования из decimal до double, но это несчастливо.

Он также нарушает раздел 6.2.1 спецификации С#:

Для преобразования из десятичного числа в float или double десятичное значение округляется до ближайшего двойного или плавающего значения. Хотя это преобразование может потерять точность, оно никогда не вызывает исключение, которое нужно сбросить.

"Ближайшее двойное значение" - это просто 8224055000... так что это ошибка ИМО. Это не тот, которого я ожидал бы получить в любое время в ближайшее время. (Он дает те же результаты в .NET 4.0b1, кстати.)

Чтобы избежать ошибки, вы, вероятно, хотите сначала нормализовать десятичное значение, эффективно "удалив" лишние 0 после десятичной точки. Это несколько сложно, так как включает 96-битную целочисленную арифметику - класс .NET 4.0 BigInteger может облегчить ее, но это может быть не для вас.

Ответ 2

Ответ заключается в том, что decimal пытается сохранить число значимых цифр. Таким образом, 8224055000.0000000000m имеет 20 значащих цифр и хранится как 82240550000000000000E-10, а 8224055000m имеет только 10 и хранится как 8224055000E+0. double мантисса (логически) 53 бит, то есть не более 16 десятичных цифр. Это точно та точность, которую вы получаете, когда вы конвертируете в double, и действительно, бродячие 1 в вашем примере находятся в 16-м знаке после запятой. Преобразование не 1-к-1, потому что double использует базу 2.

Вот двоичные представления ваших чисел:

dcm:
00000000000010100000000000000000 00000000000000000000000000000100
01110101010100010010000001111110 11110010110000000110000000000000
dbl:
0.10000011111.1110101000110001000111101101100000000000000000000001
dcm2:
00000000000000000000000000000000 00000000000000000000000000000000
00000000000000000000000000000001 11101010001100010001111011011000
dbl2 (8224055000.0):
0.10000011111.1110101000110001000111101101100000000000000000000000

Для double я использовал точки, чтобы разграничить знаки, экспоненты и поля мантиссы; для десятичного числа, см. MSDN на decimal.GetBits, но по существу последние 96 бит - мантисса. Обратите внимание, что биты мантиссы dcm2 и наиболее значимые биты dbl2 совпадают точно (не забывайте о неявном бите 1 в double мантисса), и на самом деле эти биты представляют 8224055000. Биты мантиссы из dbl такие же, как в dcm2 и dbl2, но для неприятного 1 в младшем значении бит. Показатель dcm равен 10, а мантисса - 82240550000000000000.

Обновление II: На самом деле очень легко отбросить конечные нули.

// There are 28 trailing zeros in this constant —
// no decimal can have more than 28 trailing zeros
const decimal PreciseOne = 1.000000000000000000000000000000000000000000000000m ;

// decimal.ToString() faithfully prints trailing zeroes
Assert ((8224055000.000000000m).ToString () == "8224055000.000000000") ;

// Let System.Decimal.Divide() do all the work
Assert ((8224055000.000000000m / PreciseOne).ToString () == "8224055000") ;
Assert ((8224055000.000010000m / PreciseOne).ToString () == "8224055000.00001") ;

Ответ 3

Статья Что каждый компьютерный ученый должен знать о арифметике с плавающей точкой, было бы прекрасным местом для начала.

Короткий ответ заключается в том, что двоичная арифметика с плавающей запятой обязательно является приближением, и это не всегда приближение, которое вы бы предположили. Это связано с тем, что процессоры выполняют арифметику в базе 2, в то время как люди (обычно) выполняют арифметические операции в базе 10. Из этого следует множество разнообразных неожиданных эффектов.

Ответ 4

Чтобы увидеть эту проблему более ясно проиллюстрированной, попробуйте это в LinqPad (или замените все .Dump() и измените на Console.WriteLine() s, если вам интересно).

Кажется логически неправильным для меня, что точность десятичного разряда может привести к 3 различным удвоениям. Престижность @AntonTykhyy за идею /PreciseOne:

((double)200M).ToString("R").Dump(); // 200
((double)200.0M).ToString("R").Dump(); // 200
((double)200.00M).ToString("R").Dump(); // 200
((double)200.000M).ToString("R").Dump(); // 200
((double)200.0000M).ToString("R").Dump(); // 200
((double)200.00000M).ToString("R").Dump(); // 200
((double)200.000000M).ToString("R").Dump(); // 200
((double)200.0000000M).ToString("R").Dump(); // 200
((double)200.00000000M).ToString("R").Dump(); // 200
((double)200.000000000M).ToString("R").Dump(); // 200
((double)200.0000000000M).ToString("R").Dump(); // 200
((double)200.00000000000M).ToString("R").Dump(); // 200
((double)200.000000000000M).ToString("R").Dump(); // 200
((double)200.0000000000000M).ToString("R").Dump(); // 200
((double)200.00000000000000M).ToString("R").Dump(); // 200
((double)200.000000000000000M).ToString("R").Dump(); // 200
((double)200.0000000000000000M).ToString("R").Dump(); // 200
((double)200.00000000000000000M).ToString("R").Dump(); // 200
((double)200.000000000000000000M).ToString("R").Dump(); // 200
((double)200.0000000000000000000M).ToString("R").Dump(); // 200
((double)200.00000000000000000000M).ToString("R").Dump(); // 200
((double)200.000000000000000000000M).ToString("R").Dump(); // 199.99999999999997
((double)200.0000000000000000000000M).ToString("R").Dump(); // 200
((double)200.00000000000000000000000M).ToString("R").Dump(); // 200.00000000000003
((double)200.000000000000000000000000M).ToString("R").Dump(); // 200
((double)200.0000000000000000000000000M).ToString("R").Dump(); // 199.99999999999997
((double)200.00000000000000000000000000M).ToString("R").Dump(); // 199.99999999999997

"\nFixed\n".Dump();

const decimal PreciseOne = 1.000000000000000000000000000000000000000000000000M;
((double)(200M/PreciseOne)).ToString("R").Dump(); // 200
((double)(200.0M/PreciseOne)).ToString("R").Dump(); // 200
((double)(200.00M/PreciseOne)).ToString("R").Dump(); // 200
((double)(200.000M/PreciseOne)).ToString("R").Dump(); // 200
((double)(200.0000M/PreciseOne)).ToString("R").Dump(); // 200
((double)(200.00000M/PreciseOne)).ToString("R").Dump(); // 200
((double)(200.000000M/PreciseOne)).ToString("R").Dump(); // 200
((double)(200.0000000M/PreciseOne)).ToString("R").Dump(); // 200
((double)(200.00000000M/PreciseOne)).ToString("R").Dump(); // 200
((double)(200.000000000M/PreciseOne)).ToString("R").Dump(); // 200
((double)(200.0000000000M/PreciseOne)).ToString("R").Dump(); // 200
((double)(200.00000000000M/PreciseOne)).ToString("R").Dump(); // 200
((double)(200.000000000000M/PreciseOne)).ToString("R").Dump(); // 200
((double)(200.0000000000000M/PreciseOne)).ToString("R").Dump(); // 200
((double)(200.00000000000000M/PreciseOne)).ToString("R").Dump(); // 200
((double)(200.000000000000000M/PreciseOne)).ToString("R").Dump(); // 200
((double)(200.0000000000000000M/PreciseOne)).ToString("R").Dump(); // 200
((double)(200.00000000000000000M/PreciseOne)).ToString("R").Dump(); // 200
((double)(200.000000000000000000M/PreciseOne)).ToString("R").Dump(); // 200
((double)(200.0000000000000000000M/PreciseOne)).ToString("R").Dump(); // 200
((double)(200.00000000000000000000M/PreciseOne)).ToString("R").Dump(); // 200
((double)(200.000000000000000000000M/PreciseOne)).ToString("R").Dump(); // 200
((double)(200.0000000000000000000000M/PreciseOne)).ToString("R").Dump(); // 200
((double)(200.00000000000000000000000M/PreciseOne)).ToString("R").Dump(); // 200
((double)(200.000000000000000000000000M/PreciseOne)).ToString("R").Dump(); // 200
((double)(200.0000000000000000000000000M/PreciseOne)).ToString("R").Dump(); // 200
((double)(200.00000000000000000000000000M/PreciseOne)).ToString("R").Dump(); // 200

Ответ 5

Это старая проблема, и она была предметом многих подобных вопросов в StackOverflow.

Простейшее объяснение состоит в том, что десятичные числа не могут быть точно представлены в двоичном формате

Эта ссылка является статьей, которая может объяснить проблему.