Как сделать плавающую точку

Добавил пользователь Алексей Ф.
Обновлено: 05.10.2024

В С++ существуют два типа данных с плавающей точкой: float и double . Типы данных с плавающей точкой предназначены для хранения чисел с плавающей точкой. Типы данных float и double могут хранить как положительные, так и отрицательные числа с плавающей точкой. У типа данных float размер занимаемой памяти в два раза меньше, чем у типа данных double , а значит и диапазон принимаемых значений тоже меньше. Если тип данных float объявить с приставкой long , то диапазон принимаемых значений станет равен диапазону принимаемых значений типа данных double . В основном, типы данных с плавающей точкой нужны для решения задач с высокой точностью вычислений, например, операции с деньгами.

Как хранятся действительные числа в компьютере

Для хранения действительных чисел в памяти компьютера отводится определённое количество бит. Действительное число хранится в виде знака (плюс или минус), мантиссы и экспоненты. Что такое мантисса и экспонента лучше объяснить на примере: масса Земли равна 5.972*10 24 килограмм. Здесь 5.972 — мантисса, а 24 — экспонента.

При выводе больших (или очень маленьких) чисел в программе на C++ можно увидеть на экране запись типа 5.972E23. Сначала выводится мантисса, затем — буква E, а затем — экспонента. Запись представлена в десятичной системе счисления. В таком же формате можно вводить большие или очень маленькие действительные числа. Этот формат называется экспоненциальной записью числа.

Мы будем работать с типом double (с числами двойной точности), который занимает 8 байт. Один бит отводится под знак числа, 11 под экспоненту и 52 под мантиссу. С помощью 52 бит можно хранить числа длиной до 15-16 десятичных цифр. Таким образом, независимо от того, какая у числа экспонента, правильные значения будут иметь только первые 15 цифр. В примере с массой Земли точно заданы первые 4 цифры, таким образом, погрешность составляет 10 20 килограмм. Это довольно большая погрешность. Чтобы масса Земли с точностью до первых четырёх знаков изменилась, на неё нужно дополнительно поселить миллиард миллиардов довольно упитанных людей.

Таким образом, можно сказать, что числа в компьютере хранятся не с абсолютной, а с относительной погрешностью (то есть погрешность зависит от значения хранимого числа).

То, что числа хранятся неточно, создаёт нам множество проблем.

Итак, мы рассмотрели главные моменты, касающиеся основных типов данных в С++. Осталось только показать, откуда взялись все эти диапазоны принимаемых значений и размеры занимаемой памяти. А для этого разработаем программу, которая будет вычислять основные характеристики всех, выше рассмотренных, типов данных.

Экскурс

Число с плавающей запятой представлено в следующем виде:

$$N = [s] \times m \times 2^,$$

где \(m\) — мантисса ( 23 бита — дробная часть мантиссы и неявно заданный ведущий бит, всегда равный единице, поскольку мантисса хранится в нормализованном виде), \(e\) — смещённая экспонента/порядок ( 8 бит). Один бит отводится под знак ( \(s\), бит равен нулю — число положительное, единице — отрицательное).

Тип double полностью аналогичен float , он лишь содержит больше битов мантиссы и порядка.

Рассмотрим какой-нибудь пример. Число \(35\) будет представлено в следующем виде:


Бит знака равен нулю, это говорит о том, что число положительное.

Смещённая экспонента равна \(128_\) ( \(10000000_\)), смещена она на \(127\), следовательно, в действительности получаем:

$$e = e_ - 127 = 128 - 127 = 1.$$

Нормализованная мантисса равна \([1]11000000000000000000000_\), то есть \(175_\):

$$N = m \times 2^ = 175 \times 2^ = 35.$$

Инвертируем бит знака :


Теперь число стало отрицательным: \(-35\).

Рассмотрим несколько исключений. Как представить число \(0\), если мантисса всегда хранится в нормализованном виде, то есть больше либо равна \(1\)? Решение такое — условимся обозначать \(0\), приравнивая все биты смещённого порядка и мантиссы нулю:


Ноль бывает и отрицательным:


Плюс и минус бесконечность обозначаются приравниванием всех битов смещённого порядка единице, а битов мантиссы — нулю:



В некоторых случаях результат не определён, для этого есть метка \(NaN\), что значит not a number ( не число). Приравняем все биты смещённого порядка и мантиссы единице ( по правде говоря, достаточно того, чтобы хотя бы один бит мантиссы равнялся единице):


Подводим небольшой итог. Возьмём все возможные сочетания значений всех 32 битов, уберём те варианты, которые зарезервированы для \(\pm\infty\) и метки \(NaN\), и получим все числа, которые можно хранить в переменной типа float . Их количество очень велико, но пусть это никого не обольщает; из-за особенностей представления, описанных выше, не все числа точно представимы и не все операции выполняются так, как этого можно было бы ожидать.

Более подробно о типах с плавающей запятой можно прочитать в обучающих статьях. Описанный подход к представлению чисел является компромиссом между точностью, диапазоном значений и скоростью выполнения операций с числами с плавающей запятой. Во-первых, число хранится в двоичной форме, но не всякое десятичное число можно представить в виде непериодической дроби в двоичной системе; это одна из причин потери точности ( \(01_ = 0000(1100)_\)). Во-вторых, значащих цифр ( цифр мантиссы) не так уж и много: 24 двоичных цифры, или 7,2 десятичных, поэтому придётся много округлять. Но есть и менее очевидные моменты.

Нарушение свойства ассоциативности

Вычислим \(sin(x)\), разложив эту функцию в ряд Тейлора:

Напишем небольшую функцию, которая будет реализовывать эти вычисления. Тип float вместо double выбран для того, чтобы показать, как быстро накапливается погрешность; никаких дополнительных действий не производится, код исключительно демонстрационный. Целью не является показать, что семи членов ряда недостаточно, цель — показать нарушение свойства ассоциативности операций:

хотя свойство коммутативности сохраняется:

Теперь напишем аналогичную функцию, но сделаем так, чтобы те же элементы ряда суммировались в обратном порядке.

Посмотрим на это с точки зрения машины:

\(sin(\pi) \approx 3141592 - 516771\) \(+\ 255016\) \(-\ 0599265\) \(+\ 00821459\) \(-\ 000737043\) \(+\ 0000466303,\)

\(sin(\pi) \approx 0000466303 - 000737043\) \(+\ 00821459\) \(-\ 0599265\) \(+\ 255016\) \(-\ 516771\) \(+\ 3141592.\)

До этого момента мнение компьютера и человека, казалось бы, сходится. Но результатом в первом случае окажется \(00000211327\) ( \(2.11327e005\)), а во втором случае — \(00000212193\) ( \(2.12193e005\)), совпадают лишь две значащие цифры!

Сложение ( вычитание) чисел с одинаковым порядком тоже не проходит без округлений, но погрешности, как правило, минимальны.

Большие числа

Множество чисел, представимых с помощью float ( и double ), конечно. Из этого, а также из того, что количество разрядов мантиссы весьма ограничено, сразу же следует, что очень большие числа представить не получится; придётся изобретать пути обхода переполнения. Менее очевидным следствием из способа задания числа в виде, описанном выше, является то, что пространство таких чисел не является равномерным. Что это значит?

Возьмём порядок \(e = 0\) и рассмотрим два стоящих подряд числа \(N_\) и \(N_\): у первого мантисса \(m_=[1]00000000000000000000001_\), у второго — \(m_=[1]00000000000000000000010_\).



Конвертер, где можно посмотреть побитовое представление действительного числа

Воспользуемся конвертером: \(N_=10000001_\), \(N_ = 10000002_\). Разница между ними равна \(00000001_\).

Теперь возьмём порядок \(e = 127\) и снова рассмотрим два стоящих подряд числа: у первого мантисса \(m_=[1]00000000000000000000001_\), у второго — \(m_=[1]00000000000000000000010_\).



\(N_=1701412 \times 10^_\), \(N_ = 17014122 \times 10^_\). Разница между ними равна \(00000002 \times 10^_\). В рамках этого типа данных нет никакого способа задать некоторое число \(N\), находящееся в интервале между \(N_\) и \(N_\). На секунду, \(00000002 \times 10^\) — это 20 нониллионов, иначе говоря, двойка и 31 ноль! Маленький шаг для мантиссы и огромный скачок для всего числа.

Получается, что, чем больше порядок числа, тем больше разность между ним и следующим числом, которое представимо в рамках типа float . Каждому порядку соответствует одинаковое количество значений мантиссы, следовательно — одинаковое количество представимых чисел:

  • порядок \(0\), значения: \(10. 19999999\),
  • порядок \(1\) значения: \(20. 39999998\),
  • порядок \(2\), значения: \(40. 79999995\),
  • порядок \(3\), значения: \(80. 15999999\),
  • порядок \(4\), значения: \(160. 31999998\),
  • .
  • порядок \(126\), значения: \(8507059 \times 10^. 17014117 \times 10^\),
  • порядок \(127\), значения: \(17014118 \times 10^. 34028235 \times 10^\).

Каждый из этих диапазонов разбивается на равное количество интервалов. Следовательно, от \(10\) до \(19999999\), от \(160\) до \(31999998\), от \(17014118 \times 10^\) до \(34028235 \times 10^\) находится одинаковое количество доступных значений — \((2^ - 2)\) ( поскольку мантисса, за исключением скрытого бита, имеет \(23\) бита; минус сами границы).

Всё сказанное в равной степени касается отрицательных чисел и чисел с отрицательными порядками.

Заключение

Когда в программе используются числа с плавающей запятой, необходимо обращать внимание на операции сложения и вычитания из-за нарушения свойства ассоциативности вследствие ограниченной точности типа float ( и double ). Особенно в том случае, когда порядки чисел сильно различаются. Другой проблемой является большой шаг между ближайшими представимыми числами больши́х порядков. Что и было показано.

И последнее. Когда требуется точность вычислений ( например, при финансовых расчётах), следует использовать собственные или сторонние классы чисел с фиксированной запятой. Если речь идёт о языке, поддерживающем такие типы, следует использовать их. Таковым, например, является тип decimal в языке C♯‎.

Такой метод является компромиссом между точностью и диапазоном представляемых значений. Представление чисел с плавающей точкой рассмотрим на примере чисел двойной точности (double precision). Такие числа занимают в памяти два машинных слова (8 байт на 32-битных системах). Наиболее распространенное представление описано в стандарте IEEE 754.

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

  • половинной точности (half precision) (16 бит),
  • одинарной точности (single precision) (32 бита),
  • четверной точности (quadruple precision) (128 бит),
  • расширенной точности (extended precision) (80 бит).

При выборе формата программисты идут на разумный компромисс между точностью вычислений и размером числа.

Недостатком такой записи является тот факт, что числа нельзя записать однозначно: [math] 0.01 = 0.001 \times 10^1 [/math] .

Число с плавающей точкой хранится в нормализованной форме и состоит из трех частей (в скобках указано количество бит, отводимых на каждую секцию в формате double):

  1. знак
  2. экспонента (показатель степени) (в виде целого числа в коде со сдвигом)
  3. мантисса (в нормализованной форме)

В качестве базы (основания степени) используется число [math] 2 [/math] . Экспонента хранится со сдвигом [math] -1023 [/math] .

  1. В нормализованном виде любое отличное от нуля число представимо в единственном виде. Недостатком такой записи является тот факт, что невозможно представить число 0.
  2. Так как старший бит двоичного числа, записанного в нормализованной форме, всегда равен 1, его можно опустить. Это используется в стандарте IEEE 754.
  3. В отличие от целочисленных стандартов (например, integer), имеющих равномерное распределение на всем множестве значений, числа с плавающей точкой (double, например) имеют квазиравномерное распределение.
  4. Вследствие свойства 3, числа с плавающей точкой имеют постоянную относительную погрешность (в отличие от целочисленных, которые имеют постоянную абсолютную погрешность).
  5. Очевидно, не все действительные числа возможно представить в виде числа с плавающей точкой.
  6. Точно в таком формате представимы только числа, являющиеся суммой некоторых обратных степеней двойки (не ниже -53). Остальные числа попадают в некоторый диапазон и округляются до ближайшей его границы. Таким образом, абсолютная погрешность составляет половину величины младшего бита.
  7. В формате double представимы числа в диапазоне [math] [2.3 \times 10^, 1.7 \times 10^] [/math] .

В нормализованной форме невозможно представить ноль. Для его представления в стандарте зарезервированы специальные значения мантиссы и экспоненты.

Знак
Экспонента Мантисса
0 /1 0 0 0 0 0 1, 0 0 0 0 0 0 0 0 0 0 = [math]\pm0[/math]

Согласно стандарту выполняются следующие свойства:

  • [math] +0 = -0 [/math]
  • [math] \frac< \left| x \right| >= -0\,\![/math] (если [math]x\ne0[/math] )
  • [math] (-0) \cdot (-0) = +0\,\![/math]
  • [math] \left| x \right| \cdot (-0) = -0\,\![/math]
  • [math] x + (\pm 0) = x\,\![/math]
  • [math] (-0) + (-0) = -0\,\![/math]
  • [math] (+0) + (+0) = +0\,\![/math]
  • [math] \frac<-\infty>= +0\,\![/math]
  • [math] \frac<\left|x\right|>= -\infty\,\![/math] (если [math]x\ne0[/math] )

Для приближения ответа к правильному при переполнении, в double можно записать бесконечное значение. Так же, как и в случае с нолем, для этого используются специальные значение мантиссы и экспоненты.

Знак
Экспонента Мантисса
0 /1 1 1 1 1 1 1, 0 0 0 0 0 0 0 0 0 0 = [math]\pm\infty[/math]

Бесконечное значение можно получить при переполнении или при делении ненулевого числа на ноль.

В математике встречается понятие неопределенности. В стандарте double предусмотрено псевдочисло, которое арифметическая операция может вернуть даже в случае ошибки.

Знак
Экспонента Мантисса
0 /1 1 1 1 1 1 1, 0 /1 0 /1 0 /1 0 /1 0 /1 0 /1 0 /1 0 /1 0 /1 0 /1 = [math]NaN[/math]

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

  • [math] f(NaN) = NaN [/math] , где [math] f [/math] - любая арифметическая операция
  • [math] \infty + (-\infty) = NaN [/math]
  • [math] 0 \times \infty = NaN [/math]
  • [math] \frac<\pm0><\pm0>= \frac<\pm \infty><\pm \infty>= NaN [/math]
  • [math] \sqrt = NaN [/math] , где [math] x \lt 0 [/math]

где [math] E_ [/math] - минимальное значение порядка, используемое для записи чисел (единичный сдвиг), [math] E_ - 1 [/math] - минимальное значение порядка, которое он может принимать - все биты нули, нулевой сдвиг.

Ввиду сложности, денормализованные числа обычно реализуют на программном уровне, а не на аппаратном. Из-за этого резко возрастает время работы с ними. Это недопустимо в областях, где требуется большая скорость вычислений (например, видеокарты). Так как денормализованные числа представляют числа мало отличные от нуля и мало влияют на результат, зачастую они игнорируются (что резко повышает скорость). При этом используются две концепции:

  1. Flush To Zero (FTZ) - в качестве результата возвращается нуль, как только становится понятно, что результат будет представляться в денормализованном виде.
  2. Denormals Are Zero (DAZ) - денормализованные числа, поступающие на вход, рассматриваются как нули.

Начиная с версии стандарта IEEE 754 2008 года денормализованные числа называются "субнормальными" (subnormal numbers), то есть числа, меньшие "нормальных".

Таким образом, компьютер не различает числа [math] x [/math] и [math] y [/math] , если [math] 1 \lt \frac \lt 1 + \varepsilon_m [/math] .

Мера единичной точности используется для оценки точности вычислений.


Приведем пример кода на Python, который показывает, при каком значении числа [math] x [/math] компьютер не различает числа [math] x [/math] и [math] x + 1 [/math] .

То есть [math] x = 2^ [/math] , так как мантисса числа двойной точности содержит 53 бита (в памяти хранятся 52). В C++ для расчета расстояния между двумя числами двойной точности можно воспользоваться функцией [math] \mathrm [/math] .

  • [math] a \oplus b = (a + b) (1 + \delta), |\delta| \leq \varepsilon_m [/math] ,
  • [math] a \ominus b = (a - b) (1 + \delta), |\delta| \leq \varepsilon_m [/math] ,
  • [math] a \otimes b = ab (1 + \delta), |\delta| \leq \varepsilon_m [/math] .

[math] \forall a, b, c \in D^2, \tilde = (b_x \ominus a_x) \otimes (c_y \ominus a_y) \ominus (b_y \ominus a_y) \otimes (c_x \ominus a_x) [/math]

[math] \exists \tilde \in D: [/math]

  1. [math] \tilde \gt \tilde \Rightarrow (b - a) \times (c - a) \gt 0 [/math]
  2. [math] \tilde \lt -\tilde \Rightarrow (b - a) \times (c - a) \lt 0 [/math]

Обозначим [math] v = (b - a) \times (c - a) = (b_x - a_x) (c_y - a_y) - (b_y - a_y) (c_x - a_x)[/math] .

Теперь распишем это выражение в дабловой арифметике.

[math]\tilde = (b_x \ominus a_x) \otimes (c_y \ominus a_y) \ominus (b_y \ominus a_y) \otimes (c_x \ominus a_x) = \\ = [ (b_x - a_x) (c_y - a_y) (1 + \delta_1) (1 + \delta_2) (1 + \delta_3) - \\ - (b_y - a_y) (c_x - a_x) (1 + \delta_4) (1 + \delta_5) (1 + \delta_6) ] (1 + \delta_7),[/math]

[math] |\delta_i| \leq \varepsilon_m [/math]

Заметим, что [math] v \approx \tilde [/math]

Теперь оценим абсолютную погрешность [math] \epsilon = |v - \tilde|. [/math]

[math] |v - \tilde| = |(b_x - a_x) (c_y - a_y) - (b_y - a_y) (c_x - a_x) - \\ - (b_x - a_x) (c_y - a_y) (1 + \delta_1) (1 + \delta_2) (1 + \delta_3) (1 + \delta_7) + \\ + (b_y - a_y) (c_x - a_x) (1 + \delta_4) (1 + \delta_5) (1 + \delta_6) (1 + \delta_7)| = \\ = |(b_x - a_x) (c_y - a_y) (1 - (1 + \delta_1) (1 + \delta_2) (1 + \delta_3) (1 + \delta_7)) - \\ - (b_y - a_y) (c_x - a_x) (1 - (1 + \delta_4) (1 + \delta_5) (1 + \delta_6) (1 + \delta_7))| \leq \\ \leq |(b_x - a_x) (c_y - a_y) (1 - (1 + \delta_1) (1 + \delta_2) (1 + \delta_3) (1 + \delta_7))| + \\ + |(b_y - a_y) (c_x - a_x) (1 - (1 + \delta_4) (1 + \delta_5) (1 + \delta_6) (1 + \delta_7))| = \\ = |(b_x - a_x) (c_y - a_y)| \cdot |((1 + \delta_1) (1 + \delta_2) (1 + \delta_3) (1 + \delta_7) - 1)| + \\ + |(b_y - a_y) (c_x - a_x)| \cdot |((1 + \delta_4) (1 + \delta_5) (1 + \delta_6) (1 + \delta_7) - 1)| = \\ = |(b_x - a_x) (c_y - a_y)| \cdot |\delta_1 + \delta_2 + \delta_3 + \delta_7 + \delta_1 \delta_2 \ldots| + \\ + |(b_y - a_y) (c_x - a_x)| \cdot |\delta_4 + \delta_5 + \delta_6 + \delta_7 + \delta_4 \delta_5 \ldots| \leq \\ \leq |(b_x - a_x) (c_y - a_y)| \cdot (|\delta_1| + |\delta_2| + |\delta_3| + |\delta_7| + |\delta_1 \delta_2| \ldots) + \\ + |(b_y - a_y) (c_x - a_x)| \cdot (|\delta_4| + |\delta_5| + |\delta_6| + |\delta_7| + |\delta_4 \delta_5| \ldots) \leq \\ \leq |(b_x - a_x) (c_y - a_y)| \cdot (4 \varepsilon_m + 6 \varepsilon_m^2 + 4 \varepsilon_m^3 + \varepsilon_m^4) + \\ + |(b_y - a_y) (c_x - a_x)| \cdot (4 \varepsilon_m + 6 \varepsilon_m^2 + 4 \varepsilon_m^3 + \varepsilon_m^4) = \\ = (|(b_x - a_x) (c_y - a_y)| + |(b_y - a_y) (c_x - a_x)|)(4 \varepsilon_m + 6 \varepsilon_m^2 + 4 \varepsilon_m^3 + \varepsilon_m^4)[/math]

Пусть [math] t = (|(b_x - a_x) (c_y - a_y)| + |(b_y - a_y) (c_x - a_x)|).[/math] Получаем, что

[math] \epsilon = |v - \tilde| \leq t \cdot (4 \varepsilon_m + 6 \varepsilon_m^2 + 4 \varepsilon_m^3 + \varepsilon_m^4). [/math]

[math]\tilde = (|(b_x - a_x) (c_y - a_y) (1 + \delta_1) (1 + \delta_2) (1 + \delta_3)| + \\ + |(b_y - a_y) (c_x - a_x) (1 + \delta_4) (1 + \delta_5) (1 + \delta_6)|) (1 + \delta_7) \geq \\ \geq |(b_x - a_x) (c_y - a_y) (1 - \varepsilon_m)^3)|(1 - \varepsilon_m) + \\ + |(b_y - a_y) (c_x - a_x) (1 - \varepsilon_m)^3)|(1 - \varepsilon_m) = \\ = |(b_x - a_x) (c_y - a_y)| (1 - \varepsilon_m)^4 + |(b_y - a_y) (c_x - a_x)| (1 - \varepsilon_m)^4 = \\ = (|(b_x - a_x) (c_y - a_y)| + |(b_y - a_y) (c_x - a_x)|) (1 - \varepsilon_m)^4 = t \cdot (1 - \varepsilon_m)^4[/math]

[math] t \leq \tilde \frac <(1 - \varepsilon_m)^4>= \tilde (1 + 4 \varepsilon_m + 10 \varepsilon_m^2 + 20 \varepsilon_m^3 + \cdots) [/math]

[math] \epsilon = |v - \tilde| \leq \tilde \leq \tilde (1 + 4 \varepsilon_m + 10 \varepsilon_m^2 + 20 \varepsilon_m^3 + \cdots) (4 \varepsilon_m + 6 \varepsilon_m^2 + 4 \varepsilon_m^3 + \varepsilon_m^4) [/math]

[math] \tilde \lt 8 \varepsilon_m \tilde [/math]

Заметим, что это довольно грубая оценка. Вполне можно было бы написать [math] \tilde \lt 4.25 \varepsilon_m \tilde [/math] или [math] \tilde \lt 4.5 \varepsilon_m \tilde.[/math]

Тип данных double – 64-разрядная переменная с плавающей запятой

Применение DOUBLE

Тип float раньше использовался из-за того, что он был меньше double и позволял быстрее работать с тысячами и миллионами чисел с плавающей запятой. Но вычислительная мощность новых процессоров выросла настолько, что преимуществами float перед double можно пренебречь. Многие программисты считают double типом по умолчанию для чисел с плавающей запятой.

DOUBLE, FLOAT И INT

Другие числовые типы данных — это float и int . Типы данных double и float похожи, но отличаются точностью и диапазоном:

  • Float — 32-битный тип, вмещающий семь цифр. Его диапазон — примерно от 1.5 х 215; 10 -45 до 3.4 х 10 38 ;
  • Double — 64-битный тип, вмещающий 15 или 16 цифр с диапазоном от 5.0 х 10 345 to 1.7 х 10 308 .

Int также относится к числовым типам данных, но с ним можно использовать только целые числа, которым не нужна запятая. Таким образом, int содержит только целые числа, но при этом занимает меньше места. Он позволяет быстрее совершать математические операции, а также эффективнее других типов данных использует кэш-память и пропускную способность соединения.

Пожалуйста, опубликуйте свои мнения по текущей теме статьи. За комментарии, дизлайки, лайки, подписки, отклики огромное вам спасибо!

Пожалуйста, опубликуйте свои мнения по текущей теме статьи. Мы крайне благодарны вам за ваши комментарии, подписки, отклики, лайки, дизлайки!

Читайте также: