Почему сохранение двойного выражения в переменной до того, как приведение в int может привести к разным результатам, чем к прямому литье?
Я пишу эту короткую программу для проверки преобразования из double в int:
int main() {
int a;
int d;
double b = 0.41;
/* Cast from variable. */
double c = b * 100.0;
a = (int)(c);
/* Cast expression directly. */
d = (int)(b * 100.0);
printf("c = %f \n", c);
printf("a = %d \n", a);
printf("d = %d \n", d);
return 0;
}
Вывод:
c = 41.000000
a = 41
d = 40
Почему a
и d
имеют разные значения, даже если они оба являются продуктом b
и 100
?
Ответы
Ответ 1
Стандарт C позволяет реализации C вычислять операции с плавающей запятой с большей точностью, чем номинальный тип. Например, 80-битный формат с плавающей запятой Intel может использоваться, когда тип исходного кода double
для 64-битного формата IEEE-754. В этом случае поведение можно полностью объяснить, если предположить, что реализация C использует long double
(80 бит) всякий раз, когда это возможно, и преобразуется в double
, когда это требует стандарт C.
Я предполагаю, что происходит в этом случае:
- В
double b = 0.41;
, 0.41
преобразуется в double
и сохраняется в b
. Преобразование приводит к значению, немного меньшему 0,41.
- В
double c = b * 100.0000;
, b * 100.0000
оценивается в long double
. Это дает значение чуть меньше 41.
- Это выражение используется для инициализации
c
. Стандарт C требует, чтобы в этот момент он был преобразован в double
. Поскольку значение так близко к 41, преобразование составляет ровно 41. Таким образом, c
равно 41.
-
a = (int)(c);
производит 41, как обычно.
- В
d = (int)(b * 100.000);
мы имеем такое же умножение, как и раньше. Значение такое же, как и раньше, что-то немного меньше 41. Однако это значение не присваивается или не используется для инициализации double
, поэтому никакого преобразования в double
не происходит. Вместо этого он преобразуется в int
. Поскольку значение немного меньше 41, преобразование составляет 40.
Ответ 2
Компилятор может заключить, что c
должен быть инициализирован с помощью 0.41 * 100.0
и делает это лучше, чем вычисление d
.
Ответ 3
Суть проблемы в том, что 0.41
не является точно представимой в 64-битной двоичной плавающей точке IEEE 754. Фактическое значение (с достаточной точностью для отображения соответствующей части) составляет 0.409999999999999975575...
, а 100
может быть представлено точно. Умножая их вместе, должно получиться 40.9999999999999975575...
, что опять не вполне представимо. В вероятном случае, когда режим округления близок к ближайшей, нулевой или отрицательной бесконечности, это должно быть округлено до 40.9999999999999964...
. При нажатии на int это округляется до 40
.
Однако компилятору разрешено выполнять вычисления с более высокой точностью и, в частности, может заменить умножение в присваивании c
прямым хранилищем вычисленного значения.
Изменить: я просчитал наибольшее представимое число меньше 41, правильное значение приблизительно равно 40.99999999999999289...
. Как правильно указывают и Эрик Постщил и Даниэль Фишер, даже значение, вычисленное как двойное, должно округляться до 41
, если режим округления не равен нулю или отрицательной бесконечности. Вы знаете, что такое режим округления? Это имеет значение, поскольку этот пример кода показывает:
#include <stdio.h>
#include <fenv.h>
#pragma STDC FENV_ACCESS ON
int main(void)
{
int roundMode = fegetround( );
volatile double d1;
volatile double d2;
volatile double result;
volatile int rounded;
fesetround(FE_TONEAREST);
d1 = 0.41;
d2 = 100;
result = d1 * d2;
rounded = result;
printf("nearest rounded=%i\n", rounded);
fesetround(FE_TOWARDZERO);
d1 = 0.41;
d2 = 100;
result = d1 * d2;
rounded = result;
printf("zero rounded=%i\n", rounded);
fesetround(roundMode);
return 0;
}
Вывод:
nearest rounded=41
zero rounded=40