Коммутативное свойство сложения с числами двойной точности

Рассмотрим следующий модульный тест:

// Works (sum 0.1 to 0.4)
float f1 = 0.1F + 0.2F + 0.3F + 0.4F;
Assert.AreEqual(1F, f1);

// Works too (sum 0.4 to 0.1)
float f2 = 0.4F + 0.3F + 0.2F + 0.1F;
Assert.AreEqual(1F, f2);

// Works (sum 0.1 to 0.4)
double d1 = 0.1D + 0.2D + 0.3D + 0.4D;
Assert.AreEqual(1D, d1);

// Fails! (sum 0.4 to 0.1)
double d2 = 0.4D + 0.3D + 0.2D + 0.1D;
Assert.AreEqual(1D, d2);

Все работает, как и ожидалось для типа с плавающей точкой (сумма равна 1 в обоих случаях), но при использовании double коммутативность сложения не учитывается. Действительно, сумма первого равна 1, но за второе я получаю 0,999999999.....

Я понимаю, почему результат один раз, а один раз нет (потому что некоторые числа не могут быть представлены без потери точности в базе 2), но это не объясняет, почему это работает для float, а не для double...

Может кто-нибудь объяснить это?

4 ответа

Решение
float f11 = 0;
f11 += 0.1F;//0.1
f11 += 0.2F;//0.3
f11 += 0.3F;//0.6
f11 += 0.4F;//1.0

float f2 = 0.4F + 0.3F + 0.2F + 0.1F;
float f22 = 0;
f22 += 0.4F;//0.4
f22 += 0.3F;//0.700000048
f22 += 0.2F;//0.900000036
f22 += 0.1F;//1.0

Чтобы добавить ответ astander- это то, как значения выглядят для чисел с плавающей точкой. Из-за более низкой точности (7 цифр для чисел с плавающей запятой, 14-15 для двойных) значения в конечном итоге отображаются по-разному и случайно совпадают с ожидаемыми.

Но это все - это просто совпадение! Никогда не зависи от этого. Операции с плавающей запятой являются ассоциативными и не точными. Никогда не сравнивайте поплавки или удвоенные, используя ==, всегда рассматривайте возможность использования некоторого значения маржи. Этот образец работает для 1, но для другой ценности это потерпит неудачу.

Посмотрите на ниже

        // This works (sum 0.1 to 0.4)
        double d1 = 0.1D + 0.2D + 0.3D + 0.4D;
        double d11 = 0;
        d11 += 0.1D;//0.1
        d11 += 0.2D;//0.30000000000000004
        d11 += 0.3D;//0.60000000000000009
        d11 += 0.4D;//1.0

        // This does NOT work! (sum 0.4 to 0.1)
        double d2 = 0.4D + 0.3D + 0.2D + 0.1D;
        double d22 = 0;
        d22 += 0.4D;//0.4
        d22 += 0.3D;//0.7
        d22 += 0.2D;//0.89999999999999991
        d22 += 0.1D;//0.99999999999999989

И отладка, посмотрите на отдельные шаги.

Вам нужно помнить, что

        double d2 = 0.4D + 0.3D + 0.2D + 0.1D;

также можно рассматривать как

        double d2 = (((0.4D + 0.3D) + 0.2D) + 0.1D);

Кажется, что проблема не в 2-х представлениях числа 1, а в более двух путях того, как он туда попал.

В следующих:

float f = 0.3F + 0.3F + 0.2F + 0.1F;
double d = 0.3D + 0.3D + 0.2D + 0.1D;

Результатом будет:

float f = 0.900000036f;
double d = 0.9;

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

Это известная проблема при сравнении чисел с плавающей запятой, потому что в соответствии со спецификацией C# они реализованы на основе неприятного стандарта IEEE, который вызывает такое поведение.

Так что вы никогда не должны сравнивать 2 числа с плавающей запятой или двойные в C#. Вместо этого вы должны увидеть, меньше ли их разница, чем конкретное значение дельты.

Другие вопросы по тегам