Почему сохранение двойного выражения в переменной до того, как приведение в 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