Почему порядок влияет на округление при добавлении нескольких двойных значений в C#
-
22-08-2019 - |
Вопрос
Рассмотрим следующий код 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;
Поскольку двойные значения не могут точно представлять десятичные значения, вы получаете разные результаты.