Почему порядок влияет на округление при добавлении нескольких двойных чисел в C # - PullRequest
7 голосов
/ 30 марта 2009

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

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

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

result1 всегда должен равняться result2, верно? Дело в том, что это не так. result1 равен 3.3, а result2 равен 3.3000000000000003. Единственная разница - порядок констант.

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

Edit:

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

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

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 
}

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

Ответы [ 7 ]

10 голосов
/ 30 марта 2009

Я бы ожидал, что 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 и математикой заключается в том, что "+" - это сокращение от "добавить потом раунд", а не просто добавить.

6 голосов
/ 30 марта 2009

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

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

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

4 голосов
/ 30 марта 2009

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

2 голосов
/ 30 марта 2009

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

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

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

1 голос
/ 30 марта 2009

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

Допустим, что для чисел ниже 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 в обоих, но нет гарантия.

1 голос
/ 30 марта 2009

result1 всегда должен равняться result2 право

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

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

0 голосов
/ 30 марта 2009

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

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

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

...