Вы также можете быть заинтересованы в , почему арифметика с плавающей запятой обычно не делает то, что, как вы думаете, она должна делать рядов по рандомазии.Вот только одна цитата из одной статьи, в которой рассматривается вопрос, почему компьютеры не точны при вычислениях с плавающей запятой:
Математика с плавающей запятой не является точной . Простые значения, такие как 0,1, не могут быть точно представлены с использованием двоичных чисел с плавающей запятой , а ограниченная точность чисел с плавающей запятой означает, что незначительные изменения в порядке операций или точность промежуточных звеньев может изменить результат .Это означает, что сравнивать два числа с плавающей точкой, чтобы увидеть, равны ли они, обычно не то, что вы хотите.
(...)
Вот один пример неточности, которая может закрасться:
float f = 0.1f;
float sum;
sum = 0;
for (int i = 0; i < 10; ++i)
sum += f;
float product = f * 10;
printf("sum = %1.15f, mul = %1.15f, mul2 = %1.15f\n",
sum, product, f * 10);
Этот код пытается вычислить 'один'тремя разными способами: повторное добавление и два небольших варианта умножения.Естественно, мы получаем три разных результата, и только один из них равен 1,0:
sum=1.000000119209290, mul=1.000000000000000, mul2=1.000000014901161
(...)
Вот точные значения для 0.1, float(0.1) и double (0.1):
==========================================================================
| Number | Value |
|------------|-----------------------------------------------------------|
| 0.1 | 0.1 (of course) |
| float 0.1 | 0.100000001490116119384765625 |
| double 0.1 | 0.1000000000000000055511151231257827021181583404541015625 |
==========================================================================
После того, как все решено, давайте посмотрим на результаты кода выше:
- sum =1.000000119209290: этот расчет начинается с округленного значения, а затем складывается из него десять раз с потенциальным округлением при каждом добавлении, так что есть много места для ошибки, чтобы закрасться. Окончательный результат не равен 1,0, и он не равен 10 * float (0,1).Однако это следующее представимое значение с плавающей запятой выше 1,0, поэтому оно очень близко.
- mul = 1.000000000000000: этот расчет начинается с округленного значения, а затем умножается на десять, поэтому вероятность появления ошибки может быть меньше.Оказывается, что преобразование из 0,1 в число с плавающей запятой (0,1) округляется в большую сторону, но умножение на десять происходит, в данном случае, с округлением в меньшую сторону, а иногда два округления дают право. Таким образом, мы получаем правильный ответ по неправильным причинам.Или, возможно, это неправильный ответ, поскольку на самом деле это не десятикратное число с плавающей запятой (0,1)!
- mul2 = 1.000000014901161: этот расчет начинается с округленного значения, а затем
double
-точность умножить на десять, что позволит избежать любой последующей ошибки округления.Таким образом, мы получаем другой правильный ответ - точное значение 10 * float (0.1) (которое может быть сохранено в double
, но не в float
).
Итак, ответ один неверный, но это всего лишь один float
прочь.Второй ответ является правильным (но неточным), тогда как третий ответ является полностью правильным (но кажется неправильным).
Акцент и разметка мои .В посте о рандомаксии даже предлагаются некоторые возможные решения этой проблемы неточности , но они не решают проблему (они просто перемещают неточность в разные части строки с плавающими числами).
Таким образом, при работе с арифметикой с плавающей запятой вы никогда не получите точный результат.Но есть вещи, которые вы можете сделать, чтобы повысить точность своих вычислений:
- Увеличьте число значащих бит в вашей плавающей запятой.
float
в C ++ имеют 21 значащий бит (примерно 7 значащих цифр) и double
имеют 52 значащих бита (примерно ~ 17 значащих цифр) - Уменьшитьколичество вычислений (так что
4.0*c
точнее, чем c+c+c+c
) - Попробуйте гарантировать, что вы будете точно такие же вычисления в точно такой же порядок (только тогда вы можете
==
/ !=
два значения и получить разумный результат)
Итак,Например, если вы измените свой код float
с (7 цифр точности) на double
с (17 цифр точности), вы увидите, что ваши результаты становятся более точными , а показывает больше цифр.Если вы попытаетесь использовать распараллеливание в своем коде, ваши вычисления могут (или не могут, в зависимости от реализации) выполняться в разных порядках в разных потоках / ядрах, что приводит к дико различной точности с плавающей запятой для каждого участвующего числа.
В качестве примера, вот код рандомасии, использующий double
вместо float
s:
double f = 0.1;
double sum;
sum = 0;
for (int i = 0; i < 10; ++i)
sum += f;
double product = f * 10;
printf("sum = %1.15f, mul = %1.15f, mul2 = %1.15f\n",
sum, product, f * 10);
, который выводит:
sum = 1.000000000000000, mul = 1.000000000000000, mul2 = 1.000000000000000
Что может показаться правильным, но когда вы увеличиваететочность printf от 1.15f
до 1.17f
:
sum = 0.99999999999999989, mul = 1.00000000000000000, mul2 = 1.00000000000000000
Опять же, вы можете видеть, что неточность закралась. sum
выполнили 10 операций +
, тогда как mul
и mul2
сделал одну операцию *
каждая, поэтому sum
неточность больше, чем неточность двух других.
Если даже 17 цифр точности вам не достаточно, то вас может заинтересоватьрешения произвольная точность для C ++.
Определение BigNum из википедии :
В компьютерной наукеce, арифметика произвольной точности, также называемая bignum арифметика, арифметика с множественной точностью или иногда арифметика с бесконечной точностью, указывает, что вычисления выполняются для чисел, чьи цифры точности ограничены только доступной памятьюхост-система.
(...)
Произвольная точность используется в приложениях, где скорость арифметики не является ограничивающим фактором, или, где требуются точные результаты с очень большими числами .
Опять акцент мой .
Вот связанный ответ, предлагающий библиотеку BigNum для C ++ :
Арифметическая библиотека GNU Multiple Precision делает то, что вы хотите http://gmplib.org/
Вот предыдущийкод, реализованный с использованием GMP (с использованием 64-битной точности или примерно 21 значащей цифры):
// Compile like: g++ question.cpp -o out.tmp -lgmpxx -lgmp
#include <stdio.h>
#include <gmpxx.h>
int main(){
mpf_class f("0.1", 64);
mpf_class sum("0", 64);
for (int i = 0; i < 10; ++i)
sum += f;
mpf_class product = f * 10;
printf("sum = %1.17f, mul = %1.17f, mul2 = %1.17f\n",
sum.get_d(), product.get_d(), ((mpf_class) (f * 10)).get_d());
}
Что выводит:
sum = 0.99999999999999989, mul = 0.99999999999999989, mul2 = 0.99999999999999989
Что является результатом выполнения вычисления в 64 biточности, а затем округлить до 51 бита (C ++ double
) и распечатать его.
Однако вы можете распечатать значение непосредственно из GMP:
// Compile like: g++ question.cpp -o out.tmp -lgmpxx -lgmp
#include <stdio.h>
#include <gmpxx.h>
#include <string>
int main(){
mpf_class f("0.1", 64);
mpf_class sum("0", 64);
for (int i = 0; i < 10; ++i)
sum += f;
mpf_class product = f * 10;
long exp = 10;
int base = 10;
int digits = 21;
printf("sum = %s, mul = %s, mul2 = %s\n",
sum.get_str(exp, base, digits).c_str(),
product.get_str(exp, base, digits).c_str(),
((mpf_class) (f * 10)).get_str(exp, base, digits).c_str());
}
Что выводит:
sum = 1, mul = 1, mul2 = 1
Что является более точным результатом, чем double
представление.Вы можете проверить интерфейс GMP C ++ здесь и здесь . Обратите внимание, однако, что библиотеки произвольной точности, как правило, медленнее, чем встроенные float
с или double
с. Преимущество состоит в том, что для повышения точности вам просто нужно изменить строку mpf_class variable(expression, precision);
.
Также не забудьте проверить предложение PaulMcKenzie Переполнение стека: не работает ли математика с плавающей запятой? :
Вопрос:
Рассмотримследующий код:
0.1 + 0.2 == 0.3 -> false
0.1 + 0.2 -> 0.30000000000000004
Почему происходят эти неточности?
Ответ:
Бинарная математика с плавающей точкой такая.В большинстве языков программирования он основан на стандарте IEEE 754. (...) Суть проблемы в том, что числа представляются в этом формате как целое число, умноженное на два; рациональные числа (например, 0,1, что составляет 1/10) , знаменатель которого не является степенью двойки, не может быть точно представлено .
Константы 0.2
и 0.3
в вашей программе также будут приблизительными к их истинным значениям.Бывает, что ближайший double
к 0.2
больше rational
число 0.2
, но что ближайший double
до 0.3
меньше rational
число 0.3
.Сумма 0.1
и 0.2
оказывается больше rational
число 0.3
и, следовательно, не согласен с константой в вашем коде.
Акцент и разметка мои .