Можно ли оптимизировать static_cast <float> из double, назначенного для double?

Я наткнулся на функцию, которая я считаю ненужной, и вообще меня пугает:

float coerceToFloat(double x) {
    volatile float y = static_cast<float>(x);
    return y;
}

Который затем используется так:

// double x
double y = coerceToFloat(x);

Отличается ли это когда-нибудь от простого?

double y = static_cast<float>(x);

Намерение, кажется, состоит в том, чтобы просто лишить двойного до единой точности. Пахнет чем-то написанным из-за крайней паранойи.

Ответ 1

static_cast<float>(x) требуется для удаления любой избыточной точности, создавая float. Хотя стандарт C++ обычно разрешает реализациям сохранять избыточную точность с плавающей точкой в выражениях, эта точность должна быть удалена операторами приведения и присваивания.

Лицензия на использование большей точности содержится в пункте 13 проекта N4659 C++:

Значения плавающих операндов и результаты плавающих выражений могут быть представлены с большей точностью и диапазоном, чем требуется типом; типы не изменяются при этом. 64

Сноска 64 гласит:

Операторы приведения и присваивания все еще должны выполнять свои конкретные преобразования, как описано в 8.4, 8.2.9 и 8.18.

Ответ 2

Вслед за комментарием @NathanOliver - компиляторам разрешено выполнять математические операции с плавающей точкой с более высокой точностью, чем типы операндов. Обычно на x86 это означает, что они делают все как 80-битные значения, потому что это наиболее эффективно в оборудовании. Только когда значение сохранено, оно должно быть возвращено к фактической точности типа. И даже в этом случае большинство компиляторов по умолчанию будут выполнять оптимизации, которые нарушают это правило, потому что форсирование этого изменения в точности замедляет операции с плавающей запятой. В большинстве случаев это нормально, потому что дополнительная точность не вредна. Если вы сторонник, вы можете использовать переключатель командной строки, чтобы заставить компилятор соблюдать это правило хранения, и вы можете увидеть, что ваши вычисления с плавающей запятой значительно медленнее.

В этой функции пометка переменной volatile говорит компилятору, что он не может сохранить это значение; это, в свою очередь, означает, что оно должно снизить точность входного значения, чтобы соответствовать типу, в котором оно хранится. Поэтому есть надежда, что это приведет к усечению.

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

В большинстве случаев серьезное применение с плавающей запятой - это то, что нужно, чтобы пропустить промежуточное усечение. Правило, требующее усечения при хранении, является скорее надеждой, чем реалистичным требованием.

С другой стороны, Java изначально требовала, чтобы все математические операции с плавающей точкой выполнялись с точной точностью, требуемой для соответствующих типов. Вы можете сделать это на оборудовании Intel, сказав, что не следует расширять типы fp до 80 бит. Это было встречено с громкими жалобами от числовых программ, потому что это делает вычисления намного медленнее. Вскоре Java перешла к понятию "строгий" fp и "нестрогий" fp, а серьезное сокращение числа использует нестрогий, то есть делает это так быстро, как поддерживает аппаратное обеспечение. Люди, которые хорошо понимают математику с плавающей точкой (не включая меня), хотят скорости и знают, как справиться с разницей в точности, которая получается.

Ответ 3

Некоторые компиляторы имеют такую концепцию "расширенной точности", когда двойники несут с собой более 64 бит данных. Это приводит к вычислениям с плавающей запятой, которые не соответствуют стандарту IEEE.

Приведенный выше код может быть попыткой не допустить, чтобы флаги расширенной точности на компиляторе убрали потерю точности. Такие флаги явно нарушают допущения точности значений типа double и значений с плавающей запятой. Кажется правдоподобным, что они не будут делать это с volatile переменной.

Ответ 4

Независимо от того, разрешено ли такое преобразование, оно происходит, и изменчивое назначение останавливает его.

Например, компиляция MSVC для 32-битной (то есть с использованием x87) с /Ox/fp:fast:

_x$ = 8                                       ; size = 8
float uselessCast(double) PROC                         ; uselessCast
        fld     QWORD PTR _x$[esp-4]
        ret     0
float uselessCast(double) ENDP                         ; uselessCast

_y$ = 8                                       ; size = 4
_x$ = 8                                       ; size = 8
float coerceToFloat(double) PROC                   ; coerceToFloat
        fld     QWORD PTR _x$[esp-4]
        fstp    DWORD PTR _y$[esp-4]
        fld     DWORD PTR _y$[esp-4]
        ret     0
float coerceToFloat(double) ENDP 

Где uselessCast ниже, а coerceToFloat как в вопросе.

float uselessCast(double x)
{
    return static_cast<float>(x);
}

Аналогично, GCC и Clang с -O3 -ffast-math -m32 -mfpmath=387

uselessCast(double):
    fld     QWORD PTR [esp+4]
    ret
coerceToFloat(double):
    sub     esp, 20
    fld     QWORD PTR [esp+24]
    fstp    DWORD PTR [esp+12]
    fld     DWORD PTR [esp+12]
    add     esp, 20
    ret

Godbolt ссылка для всего вышеперечисленного

Конечно, вы можете утверждать, что с /fp:fast или -ffast-math вы ничего не должны ожидать от арифметики с плавающей запятой, но вам это может понадобиться, но вы все равно сможете отказаться от избыточной точности.