Преобразование чисел с плавающей запятой одинарной точности в двойную для деления
Работая в сфере высокопроизводительных вычислений, мы по умолчанию используем числа с плавающей запятой одинарной точности (float
или же real
) когда возможно. Это потому, что вы можете выполнять больше операций в секунду, если каждая операция выполняется быстрее по отдельности.
Однако один из более старших сотрудников, с которыми я работаю, всегда настаивает на том, что (когда требуется точность) вы должны временно преобразовать данные с одинарной точностью в двойную точность, чтобы выполнить деление. То есть:
float a, b;
float ans = ((double)a)/((double)b);
или же
real :: a, b, ans
ans = real(dble(a)/dble(b))
в зависимости от языка, на котором вы работаете. На мой взгляд, это выглядит ужасно, и, честно говоря, я даже не знаю, был ли ответ в ans
будет более точным, чем если бы вы просто написали ans = a/b
в одноточечной точности.
Может кто-нибудь сказать мне, приведет ли преобразование ваших чисел до арифметики, особенно для деления, к более точному ответу? Это будет специфический для языка / компилятора вопрос, или это будет зависеть от IEEE? С какими числовыми значениями это повышение точности будет наиболее заметным?
Любые поучительные комментарии / ответы будут высоко ценится.
4 ответа
float ans = ((double)a)/((double)b);
Эта статья демонстрирует, что ans
всегда совпадает с вычисленным делением с одинарной точностью для арифметики IEEE 754 и FLT_EVAL_METHOD=0.
Когда FLT_EVAL_METHOD=1, то же свойство также тривиально верно.
Когда FLT_EVAL_METHOD=2, я не уверен. Возможно, что кто-то может интерпретировать правила как означающие, что long double
вычисление a/b
сначала должен быть округлен до double
затем float
, В этом случае он может быть менее точным, чем прямое округление из long double
в float
(последний дает правильно округленные результаты, тогда как первый может быть не в состоянии сделать это в крайне редких случаях, если только другая теорема, подобная теореме Фигероа, не покажет, что этого никогда не произойдет).
Короче говоря, для любой современной, разумной вычислительной платформы с плавающей точкой (*) это суеверие, float ans = ((double)a)/((double)b);
имеет какие-либо преимущества. Вы должны попросить старших людей, на которых вы ссылаетесь в своем вопросе, показать одну пару a, b
значений, для которых результат отличается, не говоря уже о более точном. Конечно, если они настаивают на том, что это лучше, у них не должно быть проблем с предоставлением одной единственной пары значений, для которых это имеет значение.
(*) не забудьте использовать -fexcess-precision=standard
с GCC, чтобы сохранить ваше здравомыслие
Это сильно зависит от того, какая платформа используется.
80x86 (или 8087-ая эра 1980-х), использующая инструкции не-SSE, выполняет всю свою арифметику с точностью до 80 бит (long double
или же real*10
). Это команда "store", которая перемещает результаты из числового процессора в память, которая теряет точность.
Если это не действительно скомпрометированный компилятор, максимальная точность должна
float a = something, b = something_else;
float ans = a/b;
поскольку для выполнения деления операнды одинарной точности будут расширены с точностью после загрузки, а в результате будет увеличена точность.
Если вы делали что-то более сложное и хотели поддерживать максимальную точность, не храните промежуточные результаты в переменных меньшего размера:
float a, b, c, d;
float prod_ad = a * d;
float prod_bc = b * c;
float sum_both = prod_ad + prod_bc; // less accurate
Это дает менее точный результат, чем выполнение всего за один раз, так как большинство компиляторов генерируют код, который сохраняет все промежуточные значения с повышенной точностью:
float a, b, c, d;
float sum_both = a * d + b * c; // more accurate
Опираясь на пример программы Евгения Рошки:
#include "stdio.h"
void main(void)
{
float a=73;
float b=19;
long double a1 = a;
long double b1 = b;
float ans1 = (a*a*a/b/b/b);
float ans2 = ((double)a*(double)a*(double)a/(double)b/(double)b/(double)b);
float ans3 = a1*a1*a1/b1/b1/b1;
long double ans4 = a1*a1*a1/b1/b1/b1;
printf ("plain: %.20g\n", ans1);
printf ("cast: %.20g\n", ans2);
printf ("native: %.20g\n", ans3);
printf ("full: %.20Lg\n", ans4);
}
обеспечивает, независимо от уровня оптимизации
plain: 56.716281890869140625
cast: 56.71628570556640625
native: 56.71628570556640625
full: 56.716285172765709289
Это показывает, что для тривиальных операций нет большой разницы. Тем не менее, изменение констант будет более сложной задачей:
float a=0.333333333333333333333333;
float b=0.1;
обеспечивает
plain: 37.03704071044921875
cast: 37.037036895751953125
native: 37.037036895751953125
full: 37.037038692721614131
где разница в точности показывает более выраженный эффект.
Да, преобразование в двойную точность даст вам лучшую точность (или, скажем так, точность) в делении. Можно сказать, что это зависит от IEEE, но только потому, что IEEE определяет форматы и стандарты. double
s по своей сути более точны, чем float
s, с хранением чисел, а также делением.
Чтобы ответить на ваш последний вопрос, это было бы наиболее заметно с большим a
и маленький (меньше 1) b
потому что тогда вы получите очень большой коэффициент в диапазоне, в котором все числа с плавающей запятой менее гранулированы.
Выполнение этого на x86 (GCC 4.9.3):
#include "stdio.h"
int main(int arc, char **argv)
{
float a=73;
float b=19;
float ans1 = (a*a*a/b/b/b);
float ans2 = ((double)a*(double)a*(double)a/(double)b/(double)b/(double)b);
printf("plain: %f\n", ans1);
printf("cast: %f\n", ans2);
return 0;
}
выходы:
plain: 56.716282
cast: 56.716286
Те же самые операции в калькуляторе Windows возвращают:
56.716285172765709287068085726782
Понятно, что второй результат имеет большую точность.