Подтвердить что ты не робот

Как компьютер выполняет арифметику с плавающей запятой?

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

cout << 1.0 / 3.0 <<endl;

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

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

Я вижу 1.

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

4b9b3361

Ответ 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-го формата. На самом деле, очень мало.

Ответ 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).