Подтвердить что ты не робот

Гарантируется ли сохранение поплавка при транспортировке через double в C/С++?

Предполагая IEEE-754 соответствие, является ли поплавок гарантированным сохранением при транспортировке через двойной?

Другими словами, всегда будет выполняться следующее утверждение:

int main()
{
    float f = some_random_float();
    assert(f == (float)(double)f);
}

Предположим, что f может получить любое из специальных значений, определенных IEEE, таких как NaN и Infinity.

В соответствии с IEEE существует ли случай, когда утверждение будет выполнено, но точное представление на уровне бит не сохраняется после переноса через double?

Фрагмент кода действителен как для C, так и для С++.

4b9b3361

Ответ 1

Вам даже не нужно принимать IEEE. C89 говорит в 3.1.2.5:

Набор значений типа float является подмножеством набора значений типа double

И каждый другой стандарт C и С++ говорит о эквивалентных вещах. Насколько я знаю, NaNs и бесконечности являются "значениями типа float", хотя значения с некоторыми правилами специального случая используются в качестве операндов.

Тот факт, что float → double → float conversion восстанавливает исходное значение float, следует (в общем) из того факта, что числовые преобразования сохраняют значение, если оно представлено в целевом типе.

Представления на уровне бит - это немного другое дело. Представьте себе, что существует значение float, которое имеет два разных побитовых представления. Тогда ничто в стандарте C не позволяет конвертировать поплавок → double → float из одного в другое. В IEEE, который не будет выполняться для "фактических значений", если не будут биты заполнения, но я не знаю, исключает ли IEEE единственный NaN, имеющий разные поразрядные представления. NaNs не сравниваются одинаково с самим собой, поэтому также нет стандартного способа определить, являются ли два NaN "теми же NaN" или "разными NaN", кроме того, что они могут преобразовывать их в строки. Проблема может быть спорным.

Одной вещью, на которую следует обратить внимание, являются несоответствующие режимы компиляторов, в которых они сохраняют сверхточные значения "под обложками", например, промежуточные результаты остаются в регистры с плавающей запятой и повторно используются без округления. Я не думаю, что это приведет к тому, что ваш примерный код завершится неудачно, но как только вы будете делать с плавающей точкой ==, это то, о чем вы начинаете беспокоиться.

Ответ 2

Из C99:

6.3.1.5 Реальные плавающие типы
1 Когда поплавок продвигается до двойного или длинного двойного, или двойной удваивается, его значение не изменяется.
2 Когда double понижается до float, длинный double понижается до double или float или значение, представленное в большей точности и дальности, чем требуется его семантическим типом (см. 6.3.1.8), явно преобразуется в его семантический тип, если преобразованное значение может быть представлено точно в новом типе, оно не изменяется...

Я думаю, это гарантирует вам, что float- > double- > float-преобразование будет сохранять исходное значение float.

Стандарт также определяет макросы INFINITY и NAN в 7.12 Mathematics <math.h>:

4 Макрос INFINITY расширяется до постоянного выражения типа float, представляющего положительную или беззначную бесконечность, если доступно; else к положительной константе типа float, которая переполняется во время перевода.
5 Макрос NAN определяется тогда и только тогда, когда реализация поддерживает тихие NaN для типа float. Он расширяется до постоянного выражения типа float, представляющего собой спокойный NaN.

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

Ответ 3

Утверждение не будет выполнено в режиме "с нуля до нуля" и/или denormalized-zero-zero (например, код, скомпилированный с -mfpmath = sse, -fast-math и т.д., а также по кучам компиляторов и архитектур по умолчанию, такой как компилятор Intel С++), если f денормализован.

Вы не можете создать denormalized float в этом режиме, но сценарий по-прежнему возможен:

a) Денормализованный поплавок поступает из внешнего источника.

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

Практический пример, который печатает следующее:

f = 5.87747e-39
f2 = 5.87747e-39

f = 5.87747e-39
f2 = 0
error, f != f2!

Пример работает как для VC2010, так и для GCC 4.3, но предполагает, что VC использует SSE для математики по умолчанию, а GCC использует FPU для вычисления по умолчанию. В противном случае пример может не проиллюстрировать проблему.

#include <limits>
#include <iostream>
#include <cmath>

#ifdef _MSC_VER
#include <xmmintrin.h>
#endif

template <class T>bool normal(T t)
{
    return (t != 0 || fabsf( t ) >= std::numeric_limits<T>::min());
}

void csr_flush_to_zero()
{
#ifdef _MSC_VER
    _MM_SET_FLUSH_ZERO_MODE(_MM_FLUSH_ZERO_ON);
#else
    unsigned csr = __builtin_ia32_stmxcsr();
    csr |= (1 << 15);
    __builtin_ia32_ldmxcsr(csr);
#endif
}

void test_cast(float f) 
{
    std::cout << "f = " << f << "\n";
    double d = double(f);
    float f2 = float(d);
    std::cout << "f2 = " << f2 << "\n";

    if(f != f2)
        std::cout << "error, f != f2!\n";

    std::cout << "\n";
}

int main()
{
    float f = std::numeric_limits<float>::min() / 2.0;

    test_cast(f);
    csr_flush_to_zero();
    test_cast(f);
}