Преобразование чисел с плавающей запятой одинарной точности в двойную для деления

Работая в сфере высокопроизводительных вычислений, мы по умолчанию используем числа с плавающей запятой одинарной точности (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 определяет форматы и стандарты. doubles по своей сути более точны, чем floats, с хранением чисел, а также делением.

Чтобы ответить на ваш последний вопрос, это было бы наиболее заметно с большим 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

Понятно, что второй результат имеет большую точность.

Другие вопросы по тегам