Рассмотрим два очень простых умножения ниже:
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;
А как насчет других операций (сложение, деление и остаток)? Например, при добавлении более двух положительных значений одинарной точности, использование дополнительных значащих битов двойной точности может уменьшить ошибки округления, если используется для хранения промежуточных результатов выражения.
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-точностью из-за больших ошибок округления.
обычные артеметические преобразования для типов с плавающей точкой применяются перед умножением, делением и модулем:
Обычные арифметические преобразования выполняются над операндами и определяют тип результата.
§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]
Не прямой ответ на ваш вопрос, но для постоянных значений с плавающей точкой (например, указанных в вашем вопросе) метод, который дает наименьшее количество потерь точности, будет использовать рациональное представление каждого значения в качестве целочисленного числителя. делится на целочисленный знаменатель и выполняет как можно больше умножения целых чисел перед фактическим делением с плавающей запятой.
Для значений с плавающей точкой, указанных в вашем вопросе:
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