Я читал о разнице между двойной и одинарной точностью. Однако в большинстве случаев float
и double
представляются взаимозаменяемыми, т.е. использование того или иного значения не влияет на результаты. Так ли это на самом деле? Когда плавающие и двойные числа взаимозаменяемы? В чем заключаются различия между ними?
Огромная разница.
Как следует из названия, double
имеет 2x точность float
) < [1] ; ; ; 1] ; ; ; ; <. Как правило, double
имеет 15 десятичных цифр точности, а float
- 7.
Вот как рассчитывается количество цифр:
double
имеет 52 бита мантиссы + 1 скрытый бит: log (2 < sup > 53 < / sup >) & # 247; log (10) = 15,95 цифры
float
имеет 23 бита мантиссы + 1 скрытый бит: log (2 < sup > 24 < / sup >) & # 247; log (10) = 7,22 цифры
Эта потеря точности может привести к увеличению ошибок усечения при повторных расчетах, например,.
float a = 1.f / 81;
float b = 0;
for (int i = 0; i < 729; ++ i)
b += a;
printf("%.7g\n", b); // prints 9.000023
в то время как
double a = 1.0 / 81;
double b = 0;
for (int i = 0; i < 729; ++ i)
b += a;
printf("%.15g\n", b); // prints 8.99999999999996
Кроме того, максимальное значение поплавка составляет около 3e38
, но двойное - около 1.7e308
, поэтому использование float
может ударить «бесконечность» (т.е. специальное число с плавающей запятой) гораздо проще, чем «двойной» для чего-то простого, например. вычисление факториала 60.
Во время тестирования, возможно, несколько тестовых случаев содержат эти огромные цифры, которые могут привести к сбою ваших программ, если вы используете поплавки.
Конечно, иногда даже «двойной» недостаточно точен, поэтому у нас иногда есть «длинный двойной» < sup > [1] < / sup > (вышеупомянутый пример дает 9.00000000000000066 на Mac), но все типы плавающих точек страдают от ошибок round-off, поэтому, если точность очень важна (e.g. обработка денег) вы должны использовать int
или класс дроби.
Кроме того, не используйте + =
, чтобы суммировать множество чисел с плавающей запятой, так как ошибки накапливаются быстро. Если вы используете Python, используйте fsum
. В противном случае попробуйте реализовать алгоритм суммирования Кахана.
Вот что говорится в стандартах С99 (ISO-IEC 9899 6.2.5 §10) или C++2003 (ISO-IEC 14882-2003 3.1.9 §8):
Существует три типа с плавающей точкой:
float
,double
иlong double
. Типdouble
обеспечивает точность не ниже, чем уfloat
, а типlong double
- не ниже, чем уdouble
. Множество значений типаfloat
является подмножеством множества значений типаdouble
, а множество значений типаdouble
является подмножеством множества значений типаlong double
.
Стандарт C++ добавляет:
Представление значений типов с плавающей точкой определяется реализацией.
Я бы посоветовал ознакомиться с замечательной книгой What Every Computer Scientist Should Know About Floating-Point Arithmetic, в которой подробно рассматривается стандарт IEEE для плавающей точки. Вы узнаете о деталях представления и поймете, что существует компромисс между величиной и точностью. Точность представления с плавающей точкой увеличивается по мере уменьшения величины, поэтому числа с плавающей точкой в диапазоне от -1 до 1 имеют наибольшую точность.
Используя float
и double
, мы можем написать тестовую программу:
#include <stdio.h>
#include <math.h>
void dbl_solve(double a, double b, double c)
{
double d = b*b - 4.0*a*c;
double sd = sqrt(d);
double r1 = (-b + sd) / (2.0*a);
double r2 = (-b - sd) / (2.0*a);
printf("%.5f\t%.5f\n", r1, r2);
}
void flt_solve(float a, float b, float c)
{
float d = b*b - 4.0f*a*c;
float sd = sqrtf(d);
float r1 = (-b + sd) / (2.0f*a);
float r2 = (-b - sd) / (2.0f*a);
printf("%.5f\t%.5f\n", r1, r2);
}
int main(void)
{
float fa = 1.0f;
float fb = -4.0000000f;
float fc = 3.9999999f;
double da = 1.0;
double db = -4.0000000;
double dc = 3.9999999;
flt_solve(fa, fb, fc);
dbl_solve(da, db, dc);
return 0;
}
Запуск программы дает мне:
2.00000 2.00000
2.00032 1.99968
Обратите внимание, что цифры невелики, но вы все равно получаете эффекты отмены, используя float
.
(На самом деле, вышеизложенное не является лучшим способом решения квадратичных уравнений с использованием чисел с плавающей запятой с одной или двумя точками, но ответ остается неизменным, даже если используется более стабильный метод.)
Размер чисел, участвующих в расчетах точки плавания, не самый важный. Это расчет, который выполняется, который имеет значение.
По сути, если вы выполняете вычисление, и результатом является иррациональное число или повторяющееся десятичное число, то будут ошибки округления, когда это число будет сжато в используемую вами структуру данных конечного размера. Поскольку двойной размер в два раза больше поплавка, ошибка округления будет намного меньше.
Тесты могут специально использовать номера, которые могут вызвать такую ошибку, и поэтому проверены, что вы использовали соответствующий тип в своем коде.
Тип поплавка, длиной 32 бита, имеет точность 7 цифр. Хотя он может хранить значения с очень большим или очень маленьким диапазоном (+/- 3,4 10 ^ 38 или 10 ^ -38), он имеет только 7 значащих цифр.
Тип двойной, длиной 64 бита, имеет больший диапазон (* 10 ^ +/- 308) и точность 15 цифр.
Двойной тип длиной номинально составляет 80 бит, хотя заданное соединение компилятор / OS может хранить его как 12-16 байт для целей выравнивания. Длинный дубль имеет показатель, который просто смехотворно огромен и должен иметь точность 19 цифр. Microsoft, в своей бесконечной мудрости, ограничивает длинный двойной до 8 байтов, такой же, как простой двойной.
Вообще говоря, просто используйте тип double, когда вам нужно значение / переменная с плавающей запятой. Буквенные значения с плавающей запятой, используемые в выражениях, по умолчанию будут рассматриваться как двойные, а большинство математических функций, которые возвращают значения с плавающей запятой, возвращают двойные значения. Вы избавите себя от многих головных болей и отливок, если будете использовать двойной.
Плавающие числа имеют меньшую точность, чем двойные. Хотя вы уже знаете об этом, для лучшего понимания прочитайте What WE Should Know About Floating-Point Arithmetic.
Я просто столкнулся с ошибкой, которая навсегда поняла меня и потенциально может дать вам хороший пример точности плавания.
#include <iostream>
#include <iomanip>
int main(){
for(float t=0;t<1;t+=0.01){
std::cout << std::fixed << std::setprecision(6) << t << std::endl;
}
}
Выход есть
0.000000
0.010000
0.020000
0.030000
0.040000
0.050000
0.060000
0.070000
0.080000
0.090000
0.100000
0.110000
0.120000
0.130000
0.140000
0.150000
0.160000
0.170000
0.180000
0.190000
0.200000
0.210000
0.220000
0.230000
0.240000
0.250000
0.260000
0.270000
0.280000
0.290000
0.300000
0.310000
0.320000
0.330000
0.340000
0.350000
0.360000
0.370000
0.380000
0.390000
0.400000
0.410000
0.420000
0.430000
0.440000
0.450000
0.460000
0.470000
0.480000
0.490000
0.500000
0.510000
0.520000
0.530000
0.540000
0.550000
0.560000
0.570000
0.580000
0.590000
0.600000
0.610000
0.620000
0.630000
0.640000
0.650000
0.660000
0.670000
0.680000
0.690000
0.700000
0.710000
0.720000
0.730000
0.740000
0.750000
0.760000
0.770000
0.780000
0.790000
0.800000
0.810000
0.820000
0.830000
0.839999
0.849999
0.859999
0.869999
0.879999
0.889999
0.899999
0.909999
0.919999
0.929999
0.939999
0.949999
0.959999
0.969999
0.979999
0.989999
0.999999
Как вы можете видеть после 0,83, точность значительно снижается.
Однако, если я настрою t
как двойной, такой проблемы не произойдет.
Мне потребовалось пять часов, чтобы понять эту незначительную ошибку, которая разрушила мою программу.
При использовании чисел с плавающей запятой вы не можете доверять тому, что ваши локальные тесты будут точно такими же, как тесты, которые выполняются на стороне сервера. Среда и компилятор, вероятно, отличаются в вашей локальной системе и в том, где проводятся финальные тесты. Я видел эту проблему много раз прежде в некоторых соревнованиях TopCoder, особенно если вы пытаетесь сравнить два числа с плавающей запятой.
Встроенные операции сравнения отличаются, как при сравнении 2 чисел с плавающей точкой, разницей в типе данных (т.е. плавать или удваивать) может привести к разным результатам.
В отличие от int
(целое число), float
имеет десятичную точку, как и double
.
Но разница между ними заключается в том, что «двойник» вдвое детализирован как «плавок», что означает, что он может иметь удвоенное количество чисел после десятичной точки.