Является ли добавление и умножение с плавающей запятой ассоциативным?

У меня возникла проблема, когда я добавлял три значения с плавающей запятой и сравнивал их с 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 

Почему эти ценности выглядят иначе?

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

Стандартная статья по этому вопросу – это то, что должен знать каждый компьютерный ученый о арифметике с плавающей точкой . Он дает следующий пример:

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

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

Компилятор закодирован .7 как 0x1.6666666666666p-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).

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

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

 #include #include #include 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) .

  • Странная проблема сравнения поплавков в объективе-C
  • Как красиво форматировать плавающие числа в String без ненужного десятичного числа 0?
  • Как я могу написать функцию питания самостоятельно?
  • Какой диапазон чисел может быть представлен в 16-, 32- и 64-битных системах IEEE-754?
  • Как разобрать строку на float или int в Python?
  • Float / double precision в режимах отладки / выпуска
  • Как вы печатаете EXACT значение числа с плавающей запятой?
  • Java: Почему мы должны использовать BigDecimal вместо Double в реальном мире?
  • Преобразование с двойным преобразованием без научной нотации
  • Почему арифметика с плавающей запятой в C # неточна?
  • Сравнение значений с плавающей запятой
  • Давайте будем гением компьютера.