Что точнее double или float
(Примечание: в этой статье предполагается, что читатели знают об основах информатики)
Многие новички-программисты / студенты, обучающиеся в области компьютерных наук, задают часто задаваемые вопросы, имеющие отношение к конкретной области в области компьютерных наук, которые они изучают. Большинство начинающих курсов начинаются с тем номера системы, который используется на современных компьютерах, включая двоичный, десятичный, восьмеричный а также шестнадцатеричный система. Это форматы номеров компьютеров, которые являются внутренними представлениями числовых значений на компьютерах (или калькуляторах и любых других цифровых компьютерах). Эти значения сохраняются как «группировка бит».
Поскольку мы знаем, что компьютеры представляют данные в наборах двоичных цифр (т. Е. В комбинации 1s а также 0s, такие как, 1111 представляет собой 15 в десятичной системе), имеет смысл преподавать различные форматы чисел, которые используются для представления динамического диапазона значений, поскольку они составляют основные блоки обработки вычислений / число в любой операции. Как только система чисел определена в классе (часто плохо), у студентов возникает желание перейти на разные форматы чисел в одном и том же типе (т. Е. арифметика с плавающей запятой), которые имеют определенную точность и диапазон чисел. Таким образом, они вынуждены изучать нюансы между определенными типами. Два из наиболее часто используемых типов данных: терка а также двойной, и в то время как они нацелены на одни и те же потребности (то есть, арифметика с плавающей запятой), в их внутреннем представлении и общем влиянии на расчет в программе есть определенная разница. К сожалению, многие программисты пропускают нюансы между плоскими и двойными типами данных и в конечном итоге злоупотребляют ими в тех местах, где их не следует использовать в первую очередь. В конечном итоге это приводит к просчетам в других частях программы.
В этой статье я расскажу вам разницу между float и double с примерами кода на языке программирования C. Давайте начнем!
Поплавок против Двойного … В чем дело?
Лучше проработать с использованием примеров кода. Ниже приведена операция по Float и Double через математические функции, предоставляемые на языке C:
float num1 = 1.f / 82;
float num1 = sqrt (2382719676512365.1230112312312312);
Одинарная или двойная точность?
Введение
Статья также написана для тех из вас, у кого много данных. Если вам требуется несколько чисел тут или там, просто используйте double и не забивайте себе голову!
Точность данных
У 32-битных чисел с плавающей запятой точность примерно 24 бита, то есть около 7 десятичных знаков, а у чисел с двойной точностью — 53 бита, то есть примерно 16 десятичных знаков. Насколько это много? Вот некоторые грубые оценки того, какую точность вы получаете в худшем случае при использовании float и double для измерения объектов в разных диапазонах:
Почему всегда не хранить всё с двойной точностью?
Влияние на производительность вычислений с одинарной и двойной точностью
Когда производить вычисления с увеличенной точностью
Даже если вы храните данные с одинарной точностью, в некоторых случаях уместно использовать двойную точность при вычислениях. Вот простой пример на С:
Если вы запустите этот код на десяти числах одинарной точности, то не заметите каких-либо проблем с точностью. Но если запустите на миллионе чисел, то определённо заметите. Причина в том, что точность теряется при сложении больших и маленьких чисел, а после сложения миллиона чисел, вероятно, такая ситуация встретится. Практическое правило такое: если вы складываете 10^N значений, то теряете N десятичных знаков точности. Так что при сложении тысячи (10^3) чисел теряются три десятичных знака точности. Если складывать миллион (10^6) чисел, то теряются шесть десятичных знаков (а у float их всего семь!). Решение простое: вместо этого выполнять вычисления в формате double :
Пример
Предположим, что вы хотите точно измерить какое-то значение, но ваше измерительное устройство (с неким цифровым дисплеем) показывает только три значимых разряда. Измерение переменной десять раз выдаёт следующий ряд значений:
Чтобы увеличить точность, вы решаете сложить результаты измерений и вычислить среднее значение. В этом примере используется число с плавающей запятой в base-10, у которого точность составляет точно семь десятичных знаков (похоже на 32-битный float ). С тремя значимыми разрядами это даёт нам четыре дополнительных десятичных знака точности:
В сумме уже четыре значимых разряда, с тремя свободными. Что если сложить сотню таких значений? Тогда мы получим нечто вроде такого:
Всё ещё остались два неиспользованных разряда. Если суммировать тысячу чисел?
Пока что всё хорошо, но теперь мы используем все десятичные знаки для точности. Продолжим складывать числа:
Заметьте, как мы сдвигаем меньшее число, чтобы выровнять десятичный разделитель. У нас больше нет запасных разрядов, и мы опасно приблизились к потере точности. Что если сложить сто тысяч значений? Тогда добавление новых значений будет выглядеть так:
Обратите внимание, что последний значимый разряд данных (2 в 3.12) теряется. Вот теперь потеря точности действительно происходит, поскольку мы непрерывно будем игнорировать последний разряд точности наших данных. Мы видим, что проблема возникает после сложения десяти тысяч чисел, но до ста тысяч. У нас есть семь десятичных знаков точности, а в измерениях имеются три значимых разряда. Оставшиеся четыре разряда — это четыре порядка величины, которые выполняют роль своеобразного «числового буфера». Поэтому мы можем безопасно складывать четыре порядка величины = 10000 значений без потери точности, но дальше возникнут проблемы. Поэтому правило следующее:
(Существуют численно стабильные способы сложения большого количества значений. Однако простое переключение с float на double гораздо проще и, вероятно, быстрее).
Выводы
Приложение: Что такое число с плавающей запятой?
Я обнаружил, что многие на самом деле не вникают, что такое числа с плавающей запятой, поэтому есть смысл вкратце объяснить. Я пропущу здесь мельчайшие детали о битах, INF, NaN и поднормалях, а вместо этого покажу несколько примеров чисел с плавающей запятой в base-10. Всё то же самое применимо к двоичным числам.
Вот несколько примеров чисел с плавающей запятой, все с семью десятичными разрядами (это близко к 32-битному float ).
1.875545 · 10^-18 = 0.000 000 000 000 000 001 875 545
3.141593 · 10^0 = 3.141593
2.997925 · 10^8 = 299 792 500
6.022141 · 10^23 = 602 214 100 000 000 000 000 000
Выделенная жирным часть называется мантиссой, а выделенная курсивом — экспонентой. Вкратце, точность хранится в мантиссе, а величина в экспоненте. Так как с ними работать? Ну, умножение производится просто: перемножаем мантисссы и складываем экспоненты:
Сложение немного хитрее: чтобы сложить два числа разной величины, сначала нужно сдвинуть меньшее из двух чисел таким образом, чтобы запятая находилась в одном и том же месте.
Заметьте, как мы сдвинули некоторые из значимых десятичных знаков, чтобы запятые совпадали. Другими словами, мы теряем точность, когда складываем числа разных величин.
Что точнее double или float
Как хранятся действительные числа в компьютере
Для хранения действительных чисел в памяти компьютера отводится определённое количество бит. Действительное число хранится в виде знака (плюс или минус), мантиссы и экспоненты. Что такое мантисса и экспонента лучше объяснить на примере: масса Земли равна 5.972*10 24 килограмм. Здесь 5.972 — мантисса, а 24 — экспонента.
При выводе больших (или очень маленьких) чисел в программе на C++ можно увидеть на экране запись типа 5.972E23. Сначала выводится мантисса, затем — буква E, а затем — экспонента. Запись представлена в десятичной системе счисления. В таком же формате можно вводить большие или очень маленькие действительные числа. Этот формат называется экспоненциальной записью числа.
Мы будем работать с типом double (с числами двойной точности), который занимает 8 байт. Один бит отводится под знак числа, 11 под экспоненту и 52 под мантиссу. С помощью 52 бит можно хранить числа длиной до 15-16 десятичных цифр. Таким образом, независимо от того, какая у числа экспонента, правильные значения будут иметь только первые 15 цифр. В примере с массой Земли точно заданы первые 4 цифры, таким образом, погрешность составляет 10 20 килограмм. Это довольно большая погрешность. Чтобы масса Земли с точностью до первых четырёх знаков изменилась, на неё нужно дополнительно поселить миллиард миллиардов довольно упитанных людей.
Таким образом, можно сказать, что числа в компьютере хранятся не с абсолютной, а с относительной погрешностью (то есть погрешность зависит от значения хранимого числа).
То, что числа хранятся неточно, создаёт нам множество проблем.
Итак, мы рассмотрели главные моменты, касающиеся основных типов данных в С++. Осталось только показать, откуда взялись все эти диапазоны принимаемых значений и размеры занимаемой памяти. А для этого разработаем программу, которая будет вычислять основные характеристики всех, выше рассмотренных, типов данных.
Что точнее double или float
Плавающая запятая даёт много преимуществ, но « заплыв» этот полон моментов, требующих понимания и тщательного подхода. Числа с плавающей запятой используются слишком часто, чтобы пренебрегать их глубоким пониманием. Поэтому нужно хорошо разбираться, как представлены числа с плавающей запятой и как работают вычисления с ними.
Экскурс
Число с плавающей запятой представлено в следующем виде:
$$N = [s] \times m \times 2^
где \(m\) — мантисса ( 23 бита — дробная часть мантиссы и неявно заданный ведущий бит, всегда равный единице, поскольку мантисса хранится в нормализованном виде), \(e\) — смещённая экспонента/порядок ( 8 бит). Один бит отводится под знак ( \(s\), бит равен нулю — число положительное, единице — отрицательное).
Рассмотрим какой-нибудь пример. Число \(3<,>5\) будет представлено в следующем виде:
Бит знака равен нулю, это говорит о том, что число положительное.
Смещённая экспонента равна \(128_<10>\) ( \(10000000_<2>\)), смещена она на \(127\), следовательно, в действительности получаем:
Нормализованная мантисса равна \([1<,>]11000000000000000000000_<2>\), то есть \(1<,>75_<10>\):
$$N = m \times 2^
Инвертируем бит знака :
Теперь число стало отрицательным: \(-3<,>5\).
Рассмотрим несколько исключений. Как представить число \(0\), если мантисса всегда хранится в нормализованном виде, то есть больше либо равна \(1\)? Решение такое — условимся обозначать \(0\), приравнивая все биты смещённого порядка и мантиссы нулю:
Ноль бывает и отрицательным:
Плюс и минус бесконечность обозначаются приравниванием всех битов смещённого порядка единице, а битов мантиссы — нулю:
В некоторых случаях результат не определён, для этого есть метка \(NaN\), что значит not a number ( не число). Приравняем все биты смещённого порядка и мантиссы единице ( по правде говоря, достаточно того, чтобы хотя бы один бит мантиссы равнялся единице):
Более подробно о типах с плавающей запятой можно прочитать в обучающих статьях. Описанный подход к представлению чисел является компромиссом между точностью, диапазоном значений и скоростью выполнения операций с числами с плавающей запятой. Во-первых, число хранится в двоичной форме, но не всякое десятичное число можно представить в виде непериодической дроби в двоичной системе; это одна из причин потери точности ( \(0<,>1_ <10>= 0<,>000(1100)_<2>\)). Во-вторых, значащих цифр ( цифр мантиссы) не так уж и много: 24 двоичных цифры, или 7,2 десятичных, поэтому придётся много округлять. Но есть и менее очевидные моменты.
Нарушение свойства ассоциативности
Вычислим \(sin(x)\), разложив эту функцию в ряд Тейлора:
Напишем небольшую функцию, которая будет реализовывать эти вычисления. Тип float вместо double выбран для того, чтобы показать, как быстро накапливается погрешность; никаких дополнительных действий не производится, код исключительно демонстрационный. Целью не является показать, что семи членов ряда недостаточно, цель — показать нарушение свойства ассоциативности операций:
хотя свойство коммутативности сохраняется:
Теперь напишем аналогичную функцию, но сделаем так, чтобы те же элементы ряда суммировались в обратном порядке.
Посмотрим на это с точки зрения машины:
До этого момента мнение компьютера и человека, казалось бы, сходится. Но результатом в первом случае окажется \(0<,>0000211327\) ( \(2.11327e<->005\)), а во втором случае — \(0<,>0000212193\) ( \(2.12193e<->005\)), совпадают лишь две значащие цифры!
Разгадка проста: у чисел представленного ряда шесть различных ( двоичных) порядков: \(1\), \(2\), \(1\), \(-1\), \(-4\), \(-8\), \(-12\). Когда складываются ( вычитаются) два числа одного порядка или близких порядков, потери точности, как правило, небольшие. Если бы мы складывали огромное число и много маленьких чисел одного порядка, то мы заметили бы, что лучше в плане точности сперва сложить все маленькие числа, а затем уже прибавить большое. Рассмотрим обратный сценарий: сложим большое и первое маленькое число; поскольку порядки значительно различаются, маленькое число будет ( фигурально выражаясь) « раздавлено» большим из-за приведения порядков; получилось новое большое число, не очень точное, но пока ещё достаточно близкое к точному результату; к получившемуся большому числу прибавляем второе маленькое, порядки снова значительно различаются, снова маленькое число оказывается раздавленным, уже две « жертвы». И так далее. Погрешность накопилась достаточно большая.
Сложение ( вычитание) чисел с одинаковым порядком тоже не проходит без округлений, но погрешности, как правило, минимальны.
Большие числа
Множество чисел, представимых с помощью float ( и double ), конечно. Из этого, а также из того, что количество разрядов мантиссы весьма ограничено, сразу же следует, что очень большие числа представить не получится; придётся изобретать пути обхода переполнения. Менее очевидным следствием из способа задания числа в виде, описанном выше, является то, что пространство таких чисел не является равномерным. Что это значит?
Возьмём порядок \(e = 0\) и рассмотрим два стоящих подряд числа \(N_<1>\) и \(N_<2>\): у первого мантисса \(m_<1>=[1<,>]00000000000000000000001_<2>\), у второго — \(m_<2>=[1<,>]00000000000000000000010_<2>\).
Воспользуемся конвертером: \(N_<1>=1<,>0000001_<10>\), \(N_ <2>= 1<,>0000002_<10>\). Разница между ними равна \(0<,>0000001_<10>\).
Теперь возьмём порядок \(e = 127\) и снова рассмотрим два стоящих подряд числа: у первого мантисса \(m_<3>=[1<,>]00000000000000000000001_<2>\), у второго — \(m_<4>=[1<,>]00000000000000000000010_<2>\).
\(N_<3>=1<,>701412 \times 10^<38>_<10>\), \(N_ <4>= 1<,>7014122 \times 10^<38>_<10>\). Разница между ними равна \(0<,>0000002 \times 10^<38>_<10>\). В рамках этого типа данных нет никакого способа задать некоторое число \(N\), находящееся в интервале между \(N_<3>\) и \(N_<4>\). На секунду, \(0<,>0000002 \times 10^<38>\) — это 20 нониллионов, иначе говоря, двойка и 31 ноль! Маленький шаг для мантиссы и огромный скачок для всего числа.
Каждый из этих диапазонов разбивается на равное количество интервалов. Следовательно, от \(1<,>0\) до \(1<,>9999999\), от \(16<,>0\) до \(31<,>999998\), от \(1<,>7014118 \times 10^<38>\) до \(3<,>4028235 \times 10^<38>\) находится одинаковое количество доступных значений — \((2^ <23>— 2)\) ( поскольку мантисса, за исключением скрытого бита, имеет \(23\) бита; минус сами границы).
Всё сказанное в равной степени касается отрицательных чисел и чисел с отрицательными порядками.
Заключение
Когда в программе используются числа с плавающей запятой, необходимо обращать внимание на операции сложения и вычитания из-за нарушения свойства ассоциативности вследствие ограниченной точности типа float ( и double ). Особенно в том случае, когда порядки чисел сильно различаются. Другой проблемой является большой шаг между ближайшими представимыми числами больши́х порядков. Что и было показано.
Здесь были освещены только эти два аспекта. Также следует помнить о том, что нельзя непосредственно сравнивать два числа с плавающей запятой ( if (x == y) ); что у диапазона есть ограничения; что следует использовать логарифм, когда происходят вычисления с использованием огромных чисел. Ну и совсем не следует забывать о бесконечностях и метке \(NaN\). Из экзотики — флаги компилятора, касающиеся точности промежуточных результатов, из простых вещей ( хотя и не менее коварных, чем что-либо упомянутое выше) — особенности приведения типов. В общем, в выражении « вагон и маленькая тележка» эта заметка — даже не тележка, а маленькая горстка. Маленькая, но достаточно важная.
И последнее. Когда требуется точность вычислений ( например, при финансовых расчётах), следует использовать собственные или сторонние классы чисел с фиксированной запятой. Если речь идёт о языке, поддерживающем такие типы, следует использовать их. Таковым, например, является тип decimal в языке C♯.
Урок №33. Типы данных с плавающей точкой: float, double и long double
Обновл. 11 Сен 2021 |
Типы данных с плавающей точкой
Есть три типа данных с плавающей точкой: float, double и long double. Язык C++ определяет только их минимальный размер (как и с целочисленными типами). Типы данных с плавающей точкой всегда являются signed (т.е. могут хранить как положительные, так и отрицательные числа).
Тип | Минимальный размер | Типичный размер | |
Тип данных с плавающей точкой | float | 4 байта | 4 байта |
double | 8 байт | 8 байт | |
long double | 8 байт | 8, 12 или 16 байт |
Объявление переменных разных типов данных с плавающей точкой:
Если нужно использовать целое число с переменной типа с плавающей точкой, то тогда после этого числа нужно поставить разделительную точку и нуль. Это позволяет различать переменные целочисленных типов от переменных типов с плавающей запятой:
Обратите внимание, литералы типа с плавающей точкой по умолчанию относятся к типу double. f в конце числа означает тип float.
Экспоненциальная запись
Обычно, в экспоненциальной записи, в целой части находится только одна цифра, все остальные пишутся после разделительной точки (в дробной части).
На практике экспоненциальная запись может использоваться в операциях присваивания следующим образом: