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

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

почему эти значения разные?

3 ответов


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

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

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


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

компилятор закодированных .7 as 0x1.6666666666666p-1 (это шестнадцатеричное число 1.666666666666666, умноженное на 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.ccccccccccccc8p-1. Обратите внимание, что это число имеет более 53 бит в значении: "8" является 14-й цифрой после периода. С плавающей запятой невозможно вернуть результат с таким количеством битов, поэтому он должен быть округлен до ближайшего представимого числа. В этом случай, есть два числа, которые одинаково близки, 0x1.cccccccccccccp-1 и 0x1.ccccccccccccdp-1. Когда есть связи, используется число с нулем в младшем бите мантиссы является. "c" четное, А " d " нечетное, поэтому используется "c". Конечный результат сложения-0x1.cccccccccccccp-1.

Далее, добавьте номер для .1 (0х1.999999999999ap-4) к этому. Опять же, мы масштабируем, чтобы показатели совпадали, поэтому 0x1.999999999999ap-4 становится 0x.33333333333334p-1. Затем добавить к 0х1.cccccccccccccccp-1, давая 0x1.fffffffffffff4p-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<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).