Как компьютер выполняет вычисления с плавающей запятой? - PullRequest
34 голосов
/ 17 мая 2011

Я видел длинные статьи, объясняющие, как можно хранить числа с плавающей запятой и как выполняется арифметика этих чисел, но, пожалуйста, кратко объясните почему, когда я пишу

cout << 1.0 / 3.0 <<endl;

Я вижу 0.333333 , но когда я пишу

cout << 1.0 / 3.0 + 1.0 / 3.0 + 1.0 / 3.0 << endl;

Я вижу 1 .

Как компьютер это делает? Пожалуйста, объясните только этот простой пример. Мне этого достаточно.

Ответы [ 5 ]

28 голосов
/ 17 мая 2011
17 голосов
/ 19 мая 2011

Проблема в том, что формат с плавающей запятой представляет дроби в базе 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 во многих случаях знает, был ли хотя бы один или по крайней мере 1 из оставшихся крайних правых битов одним. Если это так, то эта часть дроби больше 0,5 (в масштабе), и поэтому она округляется. Промежуточные округленные значения позволяют FPU переносить самый правый бит до целой части и, наконец, округлять до правильного ответа.

Этого не произошло, потому что кто-то добавил 0,5; FPU просто делал все возможное в рамках ограничений формата. Плавающая точка на самом деле не является неточной. Это совершенно точно, но большинство чисел, которые мы ожидаем увидеть в нашем мировоззрении с рациональными числами 10, не представлены дробной частью 2 формата. На самом деле, очень немногие.

17 голосов
/ 17 мая 2011

Давайте сделаем математику. Для краткости мы предполагаем, что у вас есть только четыре значащие (основание-2) цифры.

Конечно, поскольку gcd(2,3)=1, 1/3 является периодическим, когда он представлен в base-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:)

2 голосов
/ 17 мая 2011

Что касается этого конкретного примера: я думаю, что в настоящее время компиляторы слишком умны, и автоматически проверяют, если const результат примитивных типов будет точным, если это возможно.Мне так и не удалось обмануть g ++, выполнив такие простые вычисления, как этот.

Однако такие вещи легко обойти, используя неконстантные переменные.Тем не менее,

int d = 3;
float a = 1./d;
std::cout << d*a;

точно даст 1, хотя этого и не следует ожидать.Причина, как уже было сказано, заключается в том, что operator<< округляет ошибку.

Относительно того, почему это может быть сделано: когда вы добавляете числа одинакового размера или умножаете float на int, вы получаете в значительной степени всю точность, которую может предложить максимальный тип с плавающей точкой - это означает, что отношение ошибка / результат очень мало (другими словами, ошибки возникают в последнем десятичном знаке, при условии, что у вас есть положительная ошибка).

Итак, 3*(1./3), хотя, как поплавок, не совсем ==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 ???
0 голосов
/ 19 мая 2011

Это работает, потому что точность по умолчанию составляет 6 цифр и округляется до 6 цифр, в результате получается 1. См. 27.5.4.1 конструкторы basic_ios в черновом стандарте C ++ (n3092) .

...