В чем разница между float и double в Java?
Какая еще есть разница между типами данных float и double или я что-то в упор не вижу?
Отслеживать
14.2k 1 1 золотой знак 21 21 серебряный знак 31 31 бронзовый знак
задан 18 июл 2017 в 21:19
Yuri Migushin Yuri Migushin
61 1 1 серебряный знак 7 7 бронзовых знаков
Вставляйте код прямо в сообщение (в виде кода), а не ссылайтесь на внешние ресурсы. Сообщения должны быть самодостаточными, ссылки могут служить только дополнением.
18 июл 2017 в 22:46
1 ответ 1
Сортировка: Сброс на вариант по умолчанию
Принципиальный нюанс здесь в том, что вы оперируете десятичными числами, а float и double — бинарные. Те числа, которые вы делите и получаете в результате, непредставимы в бинарной системе счисления без периодичных дробей. Подробнее:
То, что вы наблюдаете — это не округления и обрубания, а попытки компьютера впихать невпихуемое и сделать вид, что на самом деле всё в порядке. Так как количество битов у мантиссы и экспоненты типов float и double разная, то «округления» и «обрубания» будут происходить с вашей точки зрения «непредсказуемо». Эта ситуация усугубляется ещё и тем, что точность вычислений на совести процессора, который, вообще говоря, не обязан выдавать точность до последнего знака. На другом компьютере вы можете увидеть другие результаты — если не включите специальный «предсказуемый» режим, который медленнее. Но даже в этом случае вы получите только одинаковые, но «непредсказуемые» результаты на разных компьютерах.
Когда работаете с float и double, примите как данность, что они всегда с погрешностью.
Одинарная или двойная точность?
В научных вычислениях мы часто используем числа с плавающей запятой (плавающей точкой). Эта статья представляет собой руководство по выбору правильного представления числа с плавающей запятой. В большинстве языков программирования есть два встроенных вида точности: 32-битная (одинарная точность) и 64-битная (двойная точность). В семействе языков C они известны как float и double , и здесь мы будем использовать именно такие термины. Есть и другие виды точности: half , quad и т. д. Я не буду заострять на них внимание, хотя тоже много споров возникает относительно выбора half vs float или double vs quad . Так что сразу проясним: здесь идёт речь только о 32-битных и 64-битных числах IEEE 754.
Статья также написана для тех из вас, у кого много данных. Если вам требуется несколько чисел тут или там, просто используйте double и не забивайте себе голову!
Статья разбита на две отдельные (но связанные) дискуссии: что использовать для хранения ваших данных и что использовать при вычислениях. Иногда лучше хранить данные во float , а вычисления производить в double .
Если вам это нужно, в конце статьи я добавил небольшое напоминание, как работают числа с плавающей запятой. Не стесняйтесь сначала прочитать его, а потом возвращайтесь сюда.
Точность данных
У 32-битных чисел с плавающей запятой точность примерно 24 бита, то есть около 7 десятичных знаков, а у чисел с двойной точностью — 53 бита, то есть примерно 16 десятичных знаков. Насколько это много? Вот некоторые грубые оценки того, какую точность вы получаете в худшем случае при использовании float и double для измерения объектов в разных диапазонах:
| Масштаб | Одинарная точность | Двойная точность |
|---|---|---|
| Размер комнаты | микрометр | радиус протона |
| Окружность Земли | 2,4 метра | нанометр |
| Расстояние до Солнца | 10 км | толщина человеческого волоса |
| Продолжительность суток | 5 миллисекунд | пикосекунда |
| Продолительность столетия | 3 минуты | микросекунда |
| Время от Большого взрыва | тысячелетие | минута |
(пример: используя double , мы можем представить время с момента Большого взрыва с точностью около минуты).
Итак, если вы измеряете размер квартиры, то достаточно float . Но если хотите представить координаты GPS с точностью менее метра, то понадобится double .
Почему всегда не хранить всё с двойной точностью?
Если у вас много оперативной памяти, а скорость выполнения и расход аккумулятора не являются проблемой — вы можете прямо сейчас прекратить чтение и использовать double . До свидания и хорошего вам дня!
Если же память ограничена, то причина выбора float вместо double проста: он занимает вдвое меньше места. Но даже если память не является проблемой, сохранение данных во float может оказаться значительно быстрее. Как я уже упоминал, double занимает в два раза больше места, чем float , то есть требуется в два раза больше времени для размещения, инициализации и копирования данных, если вы используете double . Более того, если вы считываете данные непредсказуемым образом (случайный доступ), то с double у вас увеличится количество промахов мимо кэша, что замедляет чтение примерно на 40% (судя по практическому правилу O(√N), что подтверждено бенчмарками).
Влияние на производительность вычислений с одинарной и двойной точностью
Если у вас хорошо подогнанный конвейер с использованием SIMD, то вы сможете удвоить производительность FLOPS, заменив double на float . Если нет, то разница может быть гораздо меньше, но сильно зависит от вашего CPU. На процессоре Intel Haswell разница между float и double маленькая, а на ARM Cortex-A9 разница большая. Исчерпывающие результаты тестов см. здесь.
Конечно, если данные хранятся в double , то мало смысла производить вычисления во float . В конце концов, зачем хранить такую точность, если вы не собираетесь её использовать? Однако обратное неправильно: может быть вполне оправдано хранить данные во float , но производить некоторые или все вычисления с двойной точностью.
Когда производить вычисления с увеличенной точностью
Даже если вы храните данные с одинарной точностью, в некоторых случаях уместно использовать двойную точность при вычислениях. Вот простой пример на С:
float sum(float* values, long long count) < float sum = 0; for (long long i = 0; i < count; ++i) < sum += values[i]; >return sum; >
Если вы запустите этот код на десяти числах одинарной точности, то не заметите каких-либо проблем с точностью. Но если запустите на миллионе чисел, то определённо заметите. Причина в том, что точность теряется при сложении больших и маленьких чисел, а после сложения миллиона чисел, вероятно, такая ситуация встретится. Практическое правило такое: если вы складываете 10^N значений, то теряете N десятичных знаков точности. Так что при сложении тысячи (10^3) чисел теряются три десятичных знака точности. Если складывать миллион (10^6) чисел, то теряются шесть десятичных знаков (а у float их всего семь!). Решение простое: вместо этого выполнять вычисления в формате double :
float sum(float* values, long long count) < double sum = 0; for (long long i = 0; i < count; ++i) < sum += values[i]; >return (float)sum; >
Скорее всего, этот код будет работать так же быстро, как и первый, но при этом не будет теряться точность. Обратите внимание, что вовсе не нужно хранить числа в double , чтобы получить преимущества увеличенной точности вычислений!
Пример
Предположим, что вы хотите точно измерить какое-то значение, но ваше измерительное устройство (с неким цифровым дисплеем) показывает только три значимых разряда. Измерение переменной десять раз выдаёт следующий ряд значений:
3.16, 3.15, 3.16, 3.18, 3.15, 3.11, 3.14, 3.11, 3.14, 3.15
Чтобы увеличить точность, вы решаете сложить результаты измерений и вычислить среднее значение. В этом примере используется число с плавающей запятой в base-10, у которого точность составляет точно семь десятичных знаков (похоже на 32-битный float ). С тремя значимыми разрядами это даёт нам четыре дополнительных десятичных знака точности:
3.160000 + 3.150000 + 3.160000 + 3.180000 + 3.150000 + 3.110000 + 3.140000 + 3.110000 + 3.140000 + 3.150000 = 31.45000
В сумме уже четыре значимых разряда, с тремя свободными. Что если сложить сотню таких значений? Тогда мы получим нечто вроде такого:
314.4300
Всё ещё остались два неиспользованных разряда. Если суммировать тысячу чисел?
3140.890
31412.87
Пока что всё хорошо, но теперь мы используем все десятичные знаки для точности. Продолжим складывать числа:
31412.87 + 3.11 = 31415.98
Заметьте, как мы сдвигаем меньшее число, чтобы выровнять десятичный разделитель. У нас больше нет запасных разрядов, и мы опасно приблизились к потере точности. Что если сложить сто тысяч значений? Тогда добавление новых значений будет выглядеть так:
314155.6 + 3.12 = 314158.7
Обратите внимание, что последний значимый разряд данных (2 в 3.12) теряется. Вот теперь потеря точности действительно происходит, поскольку мы непрерывно будем игнорировать последний разряд точности наших данных. Мы видим, что проблема возникает после сложения десяти тысяч чисел, но до ста тысяч. У нас есть семь десятичных знаков точности, а в измерениях имеются три значимых разряда. Оставшиеся четыре разряда — это четыре порядка величины, которые выполняют роль своеобразного «числового буфера». Поэтому мы можем безопасно складывать четыре порядка величины = 10000 значений без потери точности, но дальше возникнут проблемы. Поэтому правило следующее:
Если в вашем числе с плавающей запятой P разрядов (7 для float , 16 для double ) точности, а в ваших данных S разрядов значимости, то у вас остаётся P-S разрядов для манёвра и можно сложить 10^(P-S) значений без проблем с точностью. Так, если бы мы использовали 16 разрядов точности вместо 7, то могли бы сложить 10^(16-3) = 10 000 000 000 000 значений без проблем с точностью.
(Существуют численно стабильные способы сложения большого количества значений. Однако простое переключение с 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
Выделенная жирным часть называется мантиссой, а выделенная курсивом — экспонентой. Вкратце, точность хранится в мантиссе, а величина в экспоненте. Так как с ними работать? Ну, умножение производится просто: перемножаем мантисссы и складываем экспоненты:
1.111111 · 10^42 · 2.000000 · 10^7
= (1.111111 · 2.000000) · 10^(42 + 7)
= 2.222222 · 10^49
Сложение немного хитрее: чтобы сложить два числа разной величины, сначала нужно сдвинуть меньшее из двух чисел таким образом, чтобы запятая находилась в одном и том же месте.
3.141593 · 10^0 + 1.111111 · 10^-3 =
3.141593 + 0.0001111111 =
3.141593 + 0.000111 =
3.141704
Заметьте, как мы сдвинули некоторые из значимых десятичных знаков, чтобы запятые совпадали. Другими словами, мы теряем точность, когда складываем числа разных величин.
- float
- double
- плавающая запятая
- плавающая точка
- одинарная точность
- двойная точность
Чем float отличается от double java
На 32-разрядной платформе ARM, в среде разработки IAR EWB for ARM применены следующие типы float, double и long double.
float — 32-битное число с плавающей точкой. Диапазон чисел от ±1.18E-38 до ±3.39E+38, 7 десятичных разрядов, 8 бит показателя степени (экспонента) и 23 бита основания степени (мантисса).
double — 64-битное число с плавающей точкой. Диапазон чисел от ±2.23E-308 to ±1.79E+308, 15 десятичных разрядов, 11 бит показателя степени (экспонента) и 52 бита основания степени (мантисса).
long double — абсолютно то же самое, что и double.
Операции с числами в формате с плавающей точкой весьма неэффективны по затратам кода, памяти и процессорного времени, поэтому таких чисел нужно по возможности избегать, или хотя бы использовать float вместо double.
Информация получена из документа справки «IAR C/C++ Development Guide» EWARM_DevelopmentGuide.ENU.pdf, раздел «FLOATING-POINT TYPES».
[См. также]
1. Описание стандартных функций < cmath >(math.h) site:cplusplus.com .
2. Как избавиться от чисел с плавающей точкой.
Pro Java
![]()
Вещественные числа в Java представлены типами данных float и double. Как показано в таблицах выше, float является 32 битным значением с плавающей точкой, с обычной точностью, а double представляет 64 битное значение с плавающей точкой, с двойной точностью. Количество бит отведенные под представление этих чисел смотрите в таблице выше. Оба типа соответствуют стандарту IEEE 754-1985, который определяет формат чисел и арифметические операции, применимые к этим числам. Но есть и небольшие отличия от этого стандарта. К обычным вещественным числам добавляются еще четыре значения:
- положительная бесконечность, выражаемая константой POSITIVE_INFINITY и возникающая при переполнении положительного значения, например в результате операции умножения 3.0*6e307 или при делении на нуль;
- отрицательная бесконечность NEGATIVE_INFINITY, возникающая при переполнении отрицательного значения, например в результате операции умножения -3.0*6e307 или при делении на нуль отрицательного числа;
- «не число», записываемое константой NaN (Not a Number) и возникающее, например, при умножении нуля на бесконечность.
- кроме того, стандарт различает положительный и отрицательный нуль, возникающий при делении на бесконечность соответствующего знака, хотя сравнение 0.0 == -0.0 дает в результате истину, true.
Операции с бесконечностями выполняются по обычным математическим правилам. Во всем остальном вещественные типы — это обычные вещественные значения, к которым применимы все арифметические операции и операции сравнения.
Вещественные литералы
![]()
Значения с плавающей точкой можно непосредственно включать в Java программу. В такой величине за необязательной последовательностью цифр следует десятичная точка и другая последовательность цифр. Ниже представлены несколько примеров:
Литералы с плавающей точкой можно также представить в экспоненциальной, или научной, нотации, в которой за числом следует буква e или E (показатель степени) и другое число. Второе число представляет степень десятки, на которую умножается первое число . Если же число записано в шестнадцатеричном формате, то экспонента это степень двойки . Например:
1.2345E02 // 1.2345 × 10 2 , или 123.45
1e-6 // 1 × 10 -6 , или 0.000001
6.02e23 // Число Авогадро: 6.02 × 10 23
Так же с Java 6, возможно записывать в шестнадцатеричном формате:
0xFp2 // 15×2 2 =60
Литералы с плавающей точкой по умолчанию являются значениями типа double . При включении значения типа float в программу за числом следует поставить символ f или F :
double d = 6.02E23;
float f = 6.02e23f;
В принципе литералы типа double можно тоже обозначать суффиксом d или D, но это особо не имеет смысла, так как вещественные литералы всегда по умолчанию double .
Большинство вещественных чисел, по самой их природе, нельзя точно представить каким-либо конечным количеством битов. Таким образом, необходимо помнить, что значения float и double являются только приближенными значениями представляемых ими чисел. float – это 32 битное приближение, которое дает как минимум 6 значимых десятичных разрядов, а double – это 64 битное приближение, которое представляет по крайней мере 15 значимых десятичных разрядов. На практике эти числа подходят для большинства вычислений с вещественными числами.
Поскольку типы с плавающей точкой в Java могут сводить переполнение к бесконечности, антипереполнение – к нулю, а также имеют особое NaN значение, то арифметические операции с плавающей точкой никогда не генерируют исключений , даже при выполнении недопустимых операций, например при делении нуля на нуль либо при вычислении корня отрицательного числа.
Бесконечные значения с плавающей точкой ведут себя вполне логично. Например, прибавление к бесконечности или вычитание из нее любого конечного значения дает бесконечность. Поведение отрицательного нуля почти не отличается от положитель ного нуля; фактически оператор равенства == сообщает о равенстве отрицательного и положительного нуля. Единственный способ отличить отрицательный нуль от по ложительного или обычного нуля – разделить на него какоелибо число. 1.0/0.0 дает положительную бесконечность, а деление 1.0 на отрицательный нуль дает отрицательную бесконечность. И наконец, поскольку NaN не является числом, оператор == сообщает, что это значение не равно ни одному другому числу, включая само значение ! Чтобы проверить, являются ли значения float и double нечисловыми (NaN), следует вызвать методы Float.isNaN() и Double.isNaN() .
Арифметические операции
![]()
Поскольку к вещественным типам применимы все арифметические операции и сравнения, целые и вещественные значения можно смешивать в операциях. При этом правило приведения типов дополняется такими условиями:
если в операции один операнд имеет тип double, то и другой приводится к типу double;
иначе, если один операнд имеет тип float, то и другой приводится к типу float;
в противном случае действует правило приведения целых значений.
Побитовые операции с вещественными типами не поддерживаются .
Операция деление по модулю (или остаток) определяется так же как и для целочисленных типов:
a % b = a — (a / b) * b
Так же для операции деления по модулю справедливо следующее выражение:
a = ((long)(a/b))*b+(a%b)
![]()
![]()
Вычисления чисел с плавающей точкой на одном и том же, а тем более на разных процессорах могут давать несколько разные результаты, поскольку виртуальная машина java выполняет эти операции на сопроцессоре (FPU), если он присутствует на устройстве. А в сопроцессоре обычно регистры для работы с плавающей точкой 80 бит, что шире даже чем double. Поэтому, если в эти регистры положить числа double, а потом опять вернуть их в double, то результаты могут быть разные для одинаковых чисел, по умолчанию java эту проблему не решает, то есть нет гарантии, что написанный код работающий с целочисленными типами будет давать одинаковый результат на разных процессорах. Это сделано потому, что желательно использовать сопроцессор для скорости вычислений. Результаты в этом случае могут немного различаться.
Чтобы результаты были на всех процессорах одинаковые, то следует использовать ключевое слово strictfp в декларации метода, например:
public static strictfp void main(String[] args)
В данном случае все что будет происходить в методе main, будет происходить без участия сопроцессора, будет строго округляться в пределах 64 бит и результат будет одинаковым на разных процессорах.
Все математические функции из библиотеки java.lang.Math работают с числами типа double.
Ну и теперь немного практики:
![]()
И вывод этой программы: