В какой точности выполняются арифметические операции с плавающей точкой?

Рассмотрим два очень простых умножения ниже:

double result1;
long double result2;
float var1=3.1;
float var2=6.789;
double var3=87.45;
double var4=234.987;

result1=var1*var2;
result2=var3*var4;

Умножения по умолчанию выполняются с большей точностью, чем операнды? Я имею в виду, что в случае первого умножения это выполняется с двойной точностью, а в случае второго умножения в архитектуре x86 — в 80-битной расширенной точности, или мы должны сами приводить операнды в выражениях к более высокой точности, как показано ниже?

result1=(double)var1*(double)var2;
result2=(long double)var3*(long double)var4;

А как насчет других операций (сложение, деление и остаток)? Например, при добавлении более двух положительных значений одинарной точности, использование дополнительных значащих битов двойной точности может уменьшить ошибки округления, если используется для хранения промежуточных результатов выражения.

7

Решение

Точность вычислений с плавающей точкой

C ++ 11 включает в себя определение FLT_EVAL_METHOD от C99 в cfloat,

FLT_EVAL_METHOD

Возможные значения:
-1 не определено
0 оцениваю только по дальности и точности типа
1 оценивают float и double как double, и long double как long double.
2 оцениваю все как долго двойной

Если ваш компилятор определяет FLT_EVAL_METHOD как 2, то вычисления r1 а также r2, и из s1 а также s2 ниже соответственно эквивалентны:

double var3 = …;
double var4 = …;

double r1 = var3 * var4;
double r2 = (long double)var3 * (long double)var4;

long double s1 = var3 * var4;
long double s2 = (long double)var3 * (long double)var4;

Если ваш компилятор определяет FLT_EVAL_METHOD как 2, то во всех четырех приведенных выше вычислениях умножение выполняется с точностью до long double тип.

Однако, если компилятор определяет FLT_EVAL_METHOD как 0 или 1, r1 а также r2и соответственно s1 а также s2не всегда одинаковы Умножения при вычислении r1 а также s1 сделаны с точностью double, Умножения при вычислении r2 а также s2 сделаны с точностью long double,

Получение широких результатов от узких аргументов

Если вы вычисляете результаты, которые предназначены для хранения в более широком типе результата, чем тип операндов, то есть result1 а также result2 в вашем вопросе вы всегда должны преобразовывать аргументы в тип, по крайней мере, такой же ширины, как и цель, как вы делаете здесь:

result2=(long double)var3*(long double)var4;

Без этого преобразования (если вы пишете var3 * var4), если определение компилятора FLT_EVAL_METHOD 0 или 1, произведение будет вычислено с точностью до double, что является позором, так как он предназначен для хранения в long double,

Если компилятор определяет FLT_EVAL_METHOD как 2, то преобразования в (long double)var3*(long double)var4 не являются необходимыми, но они также не причиняют вреда: выражение означает одно и то же с ними и без них.

Отступление: если формат назначения такой же узкий, как аргументы, когда лучше использовать расширенную точность для промежуточных результатов?

Как это ни парадоксально, но для одной операции лучше всего округлить только один раз до целевой точности. Единственный эффект вычисления одного умножения в расширенной точности состоит в том, что результат будет округлен до расширенной точности, а затем double точность. Это делает это менее точный. Другими словами, с FLT_EVAL_METHOD 0 или 1, результат r2 выше иногда менее точно, чем r1 из-за двойного округления, и если компилятор использует IEEE 754 с плавающей точкой, никогда лучше.

Ситуация отличается для больших выражений, которые содержат несколько операций. Для них обычно лучше вычислять промежуточные результаты с повышенной точностью, либо с помощью явных преобразований, либо потому, что компилятор использует FLT_EVAL_METHOD == 2, это вопрос и его принятый ответ показывают, что при вычислениях с промежуточными вычислениями с расширенной точностью 80-бит для двоичных 64 аргументов и результатов IEEE 754, формула интерполяции u2 * (1.0 - u1) + u1 * u3 всегда дает результат между u2 а также u3 за u1 между 0 и 1. Это свойство может не выполняться для промежуточных вычислений с двоичной64-точностью из-за больших ошибок округления.

8

Другие решения

обычные артеметические преобразования для типов с плавающей точкой применяются перед умножением, делением и модулем:

Обычные арифметические преобразования выполняются над операндами и определяют тип результата.

§5.6 [expr.mul]

Аналогично для сложения и вычитания:

Обычные арифметические преобразования выполняются для операндов арифметического или перечислимого типа.

§5.7 [expr.add]

обычные арифметические преобразования для типов с плавающей запятой в стандарте изложены следующие положения:

Многие бинарные операторы, которые ожидают операнды арифметического или перечислимого типа, вызывают преобразования и выдают типы результатов аналогичным образом. Цель состоит в том, чтобы получить общий тип, который также является типом результата. Этот шаблон называется обычными арифметическими преобразованиями, которые определяются следующим образом:

[…]

— Если любой из операндов имеет тип long doubleдругой должен быть преобразован в long double,

— В противном случае, если любой из операндов doubleдругой должен быть преобразован в double,

— В противном случае, если любой из операндов floatдругой должен быть преобразован в float,

§5 [expr]

Фактическая форма / точность этих типов с плавающей запятой определяется реализацией:

Тип double обеспечивает как минимум такую ​​же точность, как floatи тип long double обеспечивает как минимум такую ​​же точность, как double, Набор значений типа float является подмножеством набора значений типа double; множество значений типа double является подмножеством набора значений типа long double, Представление значений типов с плавающей запятой определяется реализацией.

§3.9.1 [basic.fundamental]

1

  1. Для умножения с плавающей запятой: множители FP используют внутренне удвоенную ширину операндов для генерации промежуточного результата, который равен реальному результату с бесконечной точностью, а затем округляют его до целевой точности. Таким образом, вы не должны беспокоиться о умножении. Результат правильно округлен.
  2. Для сложения с плавающей запятой результат также правильно округляется, так как стандартные сумматоры FP используют дополнительные достаточные 3 защитных бита для вычисления правильно округленного результата.
  3. Для деления, остатка и других сложных функций, таких как трансцендентальные, такие как sin, log, exp и т. Д. … это зависит главным образом от архитектуры и используемых библиотек. Я рекомендую вам использовать библиотеку MPFR, если вы ищете правильно округленные результаты для деления или любой другой сложной функции.
1

Не прямой ответ на ваш вопрос, но для постоянных значений с плавающей точкой (например, указанных в вашем вопросе) метод, который дает наименьшее количество потерь точности, будет использовать рациональное представление каждого значения в качестве целочисленного числителя. делится на целочисленный знаменатель и выполняет как можно больше умножения целых чисел перед фактическим делением с плавающей запятой.

Для значений с плавающей точкой, указанных в вашем вопросе:

int var1_num = 31;
int var1_den = 10;
int var2_num = 6789;
int var2_den = 1000;
int var3_num = 8745;
int var3_den = 100;
int var4_num = 234987;
int var4_den = 1000;
double result1 = (double)(var1_num*var2_num)/(var1_den*var2_den);
long double result2 = (long double)(var3_num*var4_num)/(var3_den*var4_den);

Если какой-либо из целочисленных продуктов слишком велик для intтогда вы можете использовать более крупные целочисленные типы:

unsigned int
signed   long
unsigned long
signed   long long
unsigned long long
0