Как fp с более высокой мантиссой может представлять меньшее число? - PullRequest
0 голосов
/ 03 сентября 2018

люблю FP; каждый раз, когда я думаю, что получил это, я понимаю, что ничего не знаю об этом :)

Это пример, который я не понимаю. Я суммирую 8 раз одно и то же число (0.1) и печатаю результат как суммы, так и «оригинала»:

std::cout.precision(100);

int numIteration = 8;
double step = 0.1;
double sum = 0.0;

for(int i = 0; i < numIteration; i++) {
    sum += step;
}

std::cout << "orig stored as " << numIteration / 10.0 << std::endl;
std::cout << " sum stored as " << sum << std::endl;

0.1 хранится как 0.1000000000000000055511151231257827021181583404541015625, и я ожидаю, что после 8 сумм он будет храниться больше или равным 0.8, который хранится как 0.8000000000000000444089209850062616169452667236328125.

Но результат меня шокирует. На самом деле, после 8 сум, результат равен 0.79999999999999993338661852249060757458209991455078125, что меньше.

Также, если я проверю двоичный вывод обоих, я вижу, что сумма «выше», чем «оригинал»:

0.8 stored as binary 0 01111111110 1001100110011001100110011001100110011001100110011001 // smaller
sum stored as binary 0 01111111110 1001100110011001100110011001100110011001100110011010 // higher

Но 0.79999999999999993338661852249060757458209991455078125 <<code>0.8000000000000000444089209850062616169452667236328125.

Можете ли вы мне светить?

EDIT : извините всех, я ошибся при копировании / вставке двоичного файла. Они были правы.

Ответы [ 4 ]

0 голосов
/ 03 сентября 2018

Несмотря на то, что ответ AMA является правильным в том смысле, что округление происходит после каждого добавления, такие же неожиданности могут произойти даже для одной операции (включая умножение):

#include <iostream>

int main()
{
     const auto val1 = 0.3444444444444444
              , val2 = 0.34444444444444442;
     std::cout << (2*val1) << '\n'
               << (2*val2) << '\n';
}

(Если не указано иное, я предполагаю, что IEEE удваивается при стандартном поведении округления.)

В первой строке будет показано 0,6888888888888888 (если вы мне доверяете, чтобы подсчитать для вас, это 15x4 на входе и 15x8 на выходе) без сюрпризов. Мы предполагаем, что во второй строке указана либо дополнительная цифра, возможно, около 4, либо результат не изменился.

В действительности, однако, вторая строка покажет 0,688888888888888 9 . Это удивительно, как можно округлить 4 на последнюю цифру до на следующую старшую цифру? Это противоречит нашему представлению о том, что неравенства сохраняются, когда с обеих сторон применяется положительный масштабный коэффициент. То есть так как 2 <2,5, то 2 * 2 <2 * 2,5, затем 4 <5. Это означает, что для округления в большую сторону (в десятичной системе) в <code>2*val2 потребуется последняя цифра 5, что val2 должно быть интуитивно должно быть не менее 0,3444444444444444 25 для округления вверх.

Проблема здесь в том, что каждая система счисления имеет различное округление входов и выходов. На самом деле, в результате самого умножения не происходит округления даже в двоичном виде, однако округление происходит в обоих преобразованиях системы счисления. Двоичные представления входов:

0,01011000001011011000001011011000001011011000001011001 (val1) 0,01011000001011011000001011011000001011011000001011011 (val2)

Умножение на 2 - это просто сдвиг влево на 1, конечно, в двоичном формате, который включает в себя число с плавающей запятой (по крайней мере, если мы игнорируем возможность переполнения), поэтому выходные данные:

0.10110000010110110000010110110000010110110000010110010 (2*val1) 0.10110000010110110000010110110000010110110000010110110 (2*val2)

Последний преобразуется обратно в 0,688888888888888 88395 … (обратите внимание, что теперь есть дополнительные 8), который правильно округляется до 0,6888888888888888 9 .

В этом конкретном случае первоначальная причина неожиданного поведения заключается в том, что val2 фактически становится:

0,3444444444444444 419772821675

также с дополнительным 4, который заменяет трейлинг 2, который мы ввели, и который при удвоении вызывает округление вверх в десятичном виде.

0 голосов
/ 03 сентября 2018

Ваши двоичные представления неверны. Правильные из них:

sum = 0.79999999999999993 ... = 
0b0011111111101001100110011001100110011001100110011001100110011001

numIteration / 10.0 = 0.80000000000000004... = 
0b0011111111101001100110011001100110011001100110011001100110011010
0 голосов
/ 03 сентября 2018

С IEEE округление с плавающей точкой происходит после каждой арифметической операции. И округление может идти вверх или вниз. Если вы печатаете значение sum на каждой итерации, вы должны увидеть:

sum is 0.1000000000000000055511151231257827021181583404541015625
sum is 0.200000000000000011102230246251565404236316680908203125
sum is 0.3000000000000000444089209850062616169452667236328125
sum is 0.40000000000000002220446049250313080847263336181640625
sum is 0.5
sum is 0.59999999999999997779553950749686919152736663818359375
sum is 0.6999999999999999555910790149937383830547332763671875
sum is 0.79999999999999993338661852249060757458209991455078125

Вы предполагаете, что округление может только идти вверх. Но, поскольку «Округление до ближайшего, привязка к четному» является режимом округления по умолчанию в IEEE 754, ближайшее двоичное представляемое значение выбирается на каждой итерации, поэтому результат не обязательно должен быть больше * 1009. *.

С другой стороны

std::cout << 0.1 * 8.0 << std::endl;

Будет производить ожидаемый

0.8000000000000000444089209850062616169452667236328125

Обновление: , поскольку @Evg упоминается в комментарии, направление округления с плавающей запятой можно изменить с помощью std::fesetround.

0 голосов
/ 03 сентября 2018

В общем, существует проблема, когда вы добавляете небольшое увеличение к большой сумме. Для сохранения полного результата недостаточно точности, и некоторое значение теряется. К последней итерации цикла вы начали сталкиваться с этим.

При достаточно большой сумме и небольшом приращении сумма может не меняться вообще.

...