Как компьютер выполняет арифметику с плавающей запятой?
Я видел длинные статьи, объясняющие, как числа с плавающей запятой могут быть сохранены и как выполняется арифметика этих чисел, но, пожалуйста, кратко объясните, почему, когда я пишу
cout << 1.0 / 3.0 <<endl;
Я вижу 0.333333, но когда я пишу
cout << 1.0 / 3.0 + 1.0 / 3.0 + 1.0 / 3.0 << endl;
Я вижу 1.
Как компьютер делает это? Пожалуйста, объясните только этот простой пример. Этого достаточно для меня.
Ответы
Ответ 1
Проблема заключается в том, что формат с плавающей запятой представляет фракции в базе 2.
Первый бит бит равен ½, второй ¼, и он продолжается как 1/2 n.
И проблема заключается в том, что не каждое рациональное число (число, которое может быть выражено как отношение двух целых чисел) фактически имеет конечное представление в этом базовом формате 2.
(Это делает формат с плавающей запятой сложным для использования для денежных значений. Хотя эти значения всегда являются рациональными числами (n/100), только .00,.25,.50 и .75 фактически имеют точные представления в любом количестве цифры базовой двух фракций.
)
В любом случае, когда вы добавляете их обратно, система в конечном итоге получает шанс округлить результат до числа, которое оно может точно представлять.
В какой-то момент он обнаруживает, что добавляет номер .666... в .333... один так:
00111110 1 .o10101010 10101010 10101011
+ 00111111 0 .10101010 10101010 10101011o
------------------------------------------
00111111 1 (1).0000000 00000000 0000000x # the x isn't in the final result
Самый левый бит - это знак, следующие восемь - показатель степени, а остальные бит - это доля. Между показателем и фракцией подразумевается "1", который всегда присутствует и, следовательно, фактически не хранится, как нормализованный крайний бит. Я написал нули, которые фактически не представлены как отдельные биты как o
.
Многое произошло здесь, на каждом этапе FPU предпринял довольно героические меры для округления результата. Сохраняются две дополнительные цифры точности (за пределами того, что поместится в результате), и FPU знает во многих случаях, если таковые имеются, или, по крайней мере, 1 из оставшихся самых правых бит. Если это так, то эта часть фракции больше 0,5 (масштабируется), и поэтому она округляется. Средние округленные значения позволяют FPU переносить самый правый бит до конца к целой части и, наконец, округлять до правильного ответа.
Это не произошло, потому что кто-то добавил 0.5; FPU просто сделал все возможное в рамках ограничений формата. Плавающая точка не является, фактически, неточной. Это совершенно точно, но большинство чисел, которые мы ожидаем увидеть в нашем базовом-10, рациональном номере с мировым именем, не могут быть представлены базой 2-го формата. На самом деле, очень мало.
Ответ 2
Посмотрите статью на "Что каждый компьютерный ученый должен знать о арифметике с плавающей запятой"
Ответ 3
Давайте сделаем математику. Для краткости мы предполагаем, что у вас есть только четыре значащих (base-2) цифры.
Конечно, поскольку gcd(2,3)=1
, 1/3
является периодическим, если оно представлено в базе-2. В частности, его невозможно представить точно, поэтому нам нужно довольствоваться аппроксимацией
A := 1×1/4 + 0×1/8 + 1×1/16 + 1*1/32
который ближе к реальному значению 1/3
, чем
A' := 1×1/4 + 0×1/8 + 1×1/16 + 0×1/32
Итак, печать A
в десятичном выражении дает 0.34375 (тот факт, что вы видите 0.33333 в вашем примере, просто свидетельствует о большем количестве значащих цифр в double
).
При добавлении их три раза мы получаем
A + A + A
= ( A + A ) + A
= ( (1/4 + 1/16 + 1/32) + (1/4 + 1/16 + 1/32) ) + (1/4 + 1/16 + 1/32)
= ( 1/4 + 1/4 + 1/16 + 1/16 + 1/32 + 1/32 ) + (1/4 + 1/16 + 1/32)
= ( 1/2 + 1/8 + 1/16 ) + (1/4 + 1/16 + 1/32)
= 1/2 + 1/4 + 1/8 + 1/16 + 1/16 + O(1/32)
Термин O(1/32)
не может быть представлен в результате, поэтому он отбрасывается, и мы получаем
A + A + A = 1/2 + 1/4 + 1/8 + 1/16 + 1/16 = 1
QED:)
Ответ 4
Что касается этого конкретного примера: я думаю, что компиляторы сейчас слишком умны, и автоматически убедитесь, что результат примитивных типов const
будет точным, если это возможно. Мне не удалось обмануть g++ в простой расчет, как это неправильно.
Однако легко обойти такие вещи, используя неконстантные переменные. Тем не менее,
int d = 3;
float a = 1./d;
std::cout << d*a;
даст ровно 1, хотя этого и не следует ожидать. Причина, как уже было сказано, состоит в том, что operator<<
округляет ошибку.
Как это можно сделать: когда вы добавляете числа с аналогичным размером или умножаете a float
на int
, вы получаете почти всю точность, которую может предлагать вам тип float - это означает, что отношение ошибка/результат очень мал (другими словами, ошибки возникают в конце десятичной точки, если у вас есть положительная ошибка).
Итак, 3*(1./3)
, хотя, как float, а не точно ==1
, имеет большое правильное смещение, которое мешает operator<<
заботиться о небольших ошибках. Однако, если вы затем удалите это смещение, просто вычитая 1, плавающая запятая соскользнет прямо к ошибке, и внезапно она не станет вообще пренебрежимой. Как я уже сказал, это не происходит, если вы просто набираете 3*(1./3)-1
, потому что компилятор слишком умный, но попробуйте
int d = 3;
float a = 1./d;
std::cout << d*a << " - 1 = " << d*a - 1 << " ???\n";
Что я получаю (g++, 32-разрядный Linux)
1 - 1 = 2.98023e-08 ???
Ответ 5
Это работает, потому что точность по умолчанию - 6 цифр и округленная до 6 цифр. Результат равен 1. См. 27.5.4.1 конструкторы basic_ios в С++ draft standard ( n3092).