2012-04-29 2 views
7

У меня была проблема, когда я добавлял три значения с плавающей точкой и сравнивая их с 1.Является ли добавление и умножение с плавающей запятой ассоциативным?

cout << ((0.7 + 0.2 + 0.1)==1)<<endl;  //output is 0 
cout << ((0.7 + 0.1 + 0.2)==1)<<endl;  //output is 1 

Почему эти значения выходят разные?

+0

Ваш пример кода отличается * коммутативностью *, а не * ассоциативностью *. Версия, демонстрирующая ассоциативность, будет '(0,7 + (0,1 + 0,2))' –

+0

@MattMcNabb: + - двоичная операция. С операндами с плавающей запятой он является коммутативным, но не ассоциативным. Таким образом, если у вас есть два выражения, которые дают разные результаты, вы не можете сформировать их один из другого, применяя только коммутативность. – tmyklebu

+0

@tmyklebu ОК, поэтому это проверяет ассоциативность тогда и только тогда, когда известно, что коммутативность имеет место. (Стандарт C++, похоже, не гарантирует коммутативности). –

ответ

10

Добавление с плавающей запятой не обязательно ассоциативно. Если вы измените порядок, в котором вы добавляете вещи, это может изменить результат.

Стандартная бумага по теме: What Every Computer Scientist Should Know about Floating Point Arithmetic. Он дает следующий пример:

Другая серая зона касается интерпретации круглых скобок. Из-за ошибок округления ассоциативные законы алгебры не обязательно имеют место для чисел с плавающей запятой. Например, выражение (x + y) + z имеет совершенно другой ответ, чем x + (y + z), когда x = 1e30, y = -1e30 и z = 1 (это 1 в первом случае, 0 в последнем).

5

Что, скорее всего, с популярных в настоящее время машин и программного обеспечения, является:

Компилятор кодируется, как 0x1.6666666666666p 0,7-1 (это является шестнадцатеричным номером +1,6666666666666 умножается на 2 в степени -1), .2 как 0x1.999999999999ap-3 и .1 как 0x1.999999999999ap-4. Каждое из них представляет собой число, представимое в плавающей запятой, которое ближе всего к десятичной цифре, которую вы написали.

Обратите внимание, что каждая из этих шестнадцатеричных констант с плавающей запятой имеет ровно 53 бита в своем значении (часть «фракции», часто неточно называемая мантиссой). Шестнадцатеричная цифра для знака имеет «1» и тринадцать шестнадцатиричных цифр (по четыре бита каждая, 52 всего, 53, включая «1»), что соответствует стандарту IEEE-754 для 64-битного двоичного плавающего файла, номера точек.

Давайте добавим цифры для .7 и .2: 0x1.6666666666666p-1 и 0x1.999999999999ap-3. Во-первых, масштабируйте показатель второго числа, чтобы он соответствовал первому. Для этого мы умножим показатель на 4 (сменив «p-3» на «p-1») и умножим значение на 1/4, давая 0x0.66666666666668p-1. Затем добавьте 0x1.6666666666666p-1 и 0x0.66666666666668p-1, давая 0x1.ccccccccccccccpp-1. Обратите внимание, что это число имеет более 53 бит в значении: «8» - это 14-я цифра после периода. Плавающая точка не может вернуть результат с помощью этого большого количества бит, поэтому ее нужно округлить до ближайшего представимого числа. В этом случае есть два числа, которые одинаково близки, 0x1.cccccccccccccc-1 и 0x1.ccccccccccccdd-1. Когда есть галстук, используется число с нулем в младшем разряде значащего. «c» равно и «d» нечетно, поэтому используется «c». Конечным результатом добавления является 0x1.cccccccccccccp-1.

Затем добавьте номер для .1 (0x1.999999999999ap-4). Опять же, мы масштабируем, чтобы сопоставить показатели, поэтому 0x1.999999999999ap-4 становится 0x.33333333333334p-1. Затем добавьте это к 0x1.cccccccccccccp-1, давая 0x1.fffffffffffffff4p-1. Округление до 53 бит дает 0x1.fffffffffffffp-1, и это конечный результат «.7 + .2 + .1».

Теперь рассмотрим «.7 + .1 + .2». Для «.7 + .1» добавьте 0x1.6666666666666p-1 и 0x1.999999999999ap-4. Напомним, что последний масштабируется до 0x.33333333333334p-1. Тогда точная сумма равна 0x1.99999999999994p-1. Округление до 53 бит дает 0x1.9999999999999p-1.

Затем добавьте номер для .2 (0x1.999999999999ap-3), который масштабируется до 0x0.66666666666668p-1. Точная сумма равна 0x2.00000000000008p-1. Значения с плавающей запятой всегда масштабируются для начала с 1 (за исключением особых случаев: ноль, бесконечность и очень маленькие числа в нижней части представленного диапазона), поэтому мы настраиваем это на 0x1.00000000000004p0.Наконец, мы округлились до 53 бит, давая 0x1.0000000000000p0.

Таким образом, из-за ошибок, возникающих при округлении, «.7 + .2 + .1» возвращает 0x1.fffffffffffffp-1 (очень немного меньше 1) и «.7 + .1 + .2» возвращает 0x1.0000000000000p0 (ровно 1).

1

Умножение с плавающей запятой не является ассоциативным в C или C++.

Доказательство:

#include<stdio.h> 
#include<time.h> 
#include<stdlib.h> 
using namespace std; 
int main() { 
    int counter = 0; 
    srand(time(NULL)); 
    while(counter++ < 10){ 
     float a = rand()/100000; 
     float b = rand()/100000; 
     float c = rand()/100000; 

     if (a*(b*c) != (a*b)*c){ 
      printf("Not equal\n"); 
     } 
    } 
    printf("DONE"); 
    return 0; 
} 

В этой программе около 30% времени, (a*b)*c не равна a*(b*c).

+3

или 0% времени, если 'RAND_MAX <100000'! –

Смежные вопросы