Почему порядок влияет на округление при добавлении нескольких двойных значений в C#

StackOverflow https://stackoverflow.com/questions/696769

Вопрос

Рассмотрим следующий код C#:

double result1 = 1.0 + 1.1 + 1.2;
double result2 = 1.2 + 1.0 + 1.1;

if (result1 == result2)
{
    ...
}

результат1 всегда должен быть равен результату2, верно?Дело в том, что это не так.результат1 равен 3,3, а результат2 равен 3,3000000000000003.Единственное отличие — порядок констант.

Я знаю, что двойные значения реализованы таким образом, что могут возникнуть проблемы с округлением.Я знаю, что вместо этого могу использовать десятичные дроби, если мне нужна абсолютная точность.Или что я могу использовать Math.Round() в своем операторе if.Я просто ботаник, который хочет понять, что делает компилятор C#.Может ли кто-нибудь сказать мне?

Редактировать:

Спасибо всем, кто до сих пор предлагал почитать об арифметике с плавающей запятой и/или говорил о внутренней неточности того, как процессор обрабатывает двойные числа.Но я чувствую, что основная суть моего вопроса все еще остается без ответа.Это моя вина, что я не правильно выразился.Позвольте мне выразить это так:

Разбирая приведенный выше код, я ожидаю, что будут происходить следующие операции:

double r1 = 1.1 + 1.2;
double r2 = 1.0 + r1
double r3 = 1.0 + 1.1
double r4 = 1.2 + r3

Предположим, что каждое из приведенных выше дополнений имело ошибку округления (номера e1..e4).Таким образом, r1 содержит ошибку округления e1, r2 содержит ошибки округления e1 + e2, r3 содержит e3 и r4 содержит e3 + e4.

Я не знаю, как именно происходят ошибки округления, но я ожидал, что e1+e2 будет равно e3+e4.Понятно, что нет, но мне это кажется каким-то неправильным.Другое дело, что когда я запускаю приведенный выше код, я не получаю никаких ошибок округления.Вот что заставляет меня думать, что что-то странное делает компилятор C#, а не процессор.

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

Редактировать 2

Глядя на IL из моего исходного примера кода, становится ясно, что это делает компилятор, а не процессор:

.method private hidebysig static void Main(string[] args) cil managed
{
.entrypoint
    .maxstack 1
    .locals init (
        [0] float64 result1,
        [1] float64 result2)
    L_0000: nop 
    L_0001: ldc.r8 3.3
    L_000a: stloc.0 
    L_000b: ldc.r8 3.3000000000000003
    L_0014: stloc.1 
    L_0015: ret 
}

Компилятор складывает числа за меня!

Это было полезно?

Решение

Я ожидал, что e1+e2 будет равно e3+e4.

Это не совсем похоже на ожидание

 floor( 5/3 ) + floor( 2/3 + 1 )

в равной

 floor( 5/3 + 2/3 ) + floor( 1 )

за исключением того, что вы умножаете на 2^53, прежде чем взять слово.

Использование 12-битной точности с плавающей запятой и усечение ваших значений:

1.0            =  1.00000000000
1.1            =  1.00011001100
1.2            =  1.00110011001

1.0 + 1.1      = 10.00011001100 // extended during sum
r1 = 1.0 + 1.1 = 10.0001100110  // truncated to 12 bit
r1  + 1.2      = 11.01001100101 // extended during sum
r2 = r1  + 1.2 = 11.0100110010  // truncated to 12 bit

1.1 + 1.2      = 10.01001100110 // extended during sum
r3 = 1.1 + 1.2 = 10.0100110011  // truncated to 12 bit
r3 + 1.0       = 11.01001100110 // extended during sum
r4 = r3  + 1.0 = 11.0100110011  // truncated to 12 bit

Таким образом, изменение порядка операций/усечения приводит к изменению ошибки, и r4 != r2.Если вы добавите 1,1 и 1,2 в этой системе, последний бит будет перенесен, поэтому он не потеряется при усечении.Если вы добавите 1,0 к 1,1, последний бит 1,1 будет потерян, и результат будет другим.

В одном заказе округление (путем усечения) удаляет конечный знак. 1.

В другом порядке округление удаляет конечный знак. 0 оба раза.

Один не равен нулю;так что ошибки не те же.

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

Разница между fp и математикой заключается в том, что + — это сокращение от «добавить, затем округлить», а не просто добавить.

Другие советы

Компилятор С# ничего не делает.Процессор есть.

если у вас есть A в регистре ЦП, а затем вы добавляете B, результат, сохраненный в этом регистре, равен A + B, аппроксимированный используемой плавающей точностью

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

Видеть классическая статья (Что должен знать каждый ученый-компьютерщик об арифметике с плавающей запятой) на предмет.Подобные вещи происходят с арифметикой с плавающей запятой.Нужен ученый-компьютерщик, чтобы сказать вам, что 1/3+1/3+1/3 это не равен 1...

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

double epsilon = 0.0000001;
if (abs(result1 - result2) <= epsilon)
{
    ...
}

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

Результат1 должен всегда равный результат2, верно?

Неправильный.Это верно в математике, но не в арифметика с плавающей запятой.

Вам нужно будет прочитать некоторые Учебник по численному анализу.

Почему ошибки не одинаковы в зависимости от порядка, можно объяснить на другом примере.

Допустим, для чисел ниже 10 он может хранить все числа, то есть 1, 2, 3 и так далее до 10 включительно, но после 10 он может хранить только каждое второе число из-за внутренней потери. Другими словами, он может хранить только 10, 12, 14 и т. д.

Теперь, на этом примере, вы поймете, почему следующее дает разные результаты:

1 + 1 + 1 + 10 = 12 (or 14, depending on rounding)
10 + 1 + 1 + 1 = 10

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

Например, 3.00000000003 + 3.00000000003 может оказаться 6.00000000005 (обратите внимание, что в конце нет цифры 6), но 3.00000000003 + 2.99999999997 может оказаться 6.00000000001, и при этом:

step 1: 3.00000000003 + 3.00000000003 = 6.00000000005
step 2: 6.00000000005 + 2.99999999997 = 9.00000000002

но измените порядок:

step 1: 3.00000000003 + 2.99999999997 = 6.00000000001
step 2: 6.00000000001 + 3.00000000003 = 9.00000000004

Так что это будет иметь значение.

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

На самом деле вы используете разные значения, потому что промежуточные результаты разные:

double result1 = 2.1 + 1.2;
double result2 = 2.2 + 1.1;

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

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top