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

Как бесконечность представлена ​​в C двойной?

Я узнал из книги Computer Systems: "Перспектива программиста", согласно которой стандарт IEEE требует, чтобы число с плавающей запятой двойной точности представлялось с использованием следующего 64-битного двоичного формата:

  • s: 1 бит для знака
  • exp: 11 бит для экспоненты
  • frac: 52 бит для фракции

Бесконечность + представляется как специальное значение со следующим шаблоном:

  • s = 0
  • все биты exp: 1
  • бит всех фракций равен 0

И я думаю, что полный 64-бит для double должен быть в следующем порядке:

(ы) (ехр) (ГРП)

Итак, я пишу следующий код C, чтобы проверить его:

//Check the infinity
double x1 = (double)0x7ff0000000000000;  // This should be the +infinity
double x2 = (double)0x7ff0000000000001; //  Note the extra ending 1, x2 should be NaN
printf("\nx1 = %f, x2 = %f sizeof(double) = %d", x1,x2, sizeof(x2));
if (x1 == x2)
    printf("\nx1 == x2");
else
    printf("\nx1 != x2");

Но результат:

x1 = 9218868437227405300.000000, x2 = 9218868437227405300.000000 sizeof(double) = 8
x1 == x2

Почему число допустимое число, а не некоторая ошибка бесконечности?

Почему x1 == x2?

(Я использую компилятор MinGW GCC.)

ADD 1

Я изменил код, как показано ниже, и успешно подтвердил бесконечность и NaN.

//Check the infinity and NaN
unsigned long long x1 = 0x7ff0000000000000ULL; // +infinity as double
unsigned long long x2 = 0xfff0000000000000ULL; // -infinity as double
unsigned long long x3 = 0x7ff0000000000001ULL; // NaN as double
double y1 =* ((double *)(&x1));
double y2 =* ((double *)(&x2));
double y3 =* ((double *)(&x3));

printf("\nsizeof(long long) = %d", sizeof(x1));
printf("\nx1 = %f, x2 = %f, x3 = %f", x1, x2, x3); // %f is good enough for output
printf("\ny1 = %f, y2 = %f, y3 = %f", y1, y2, y3);

Результат:

sizeof(long long) = 8
x1 = 1.#INF00, x2 = -1.#INF00, x3 = 1.#SNAN0
y1 = 1.#INF00, y2 = -1.#INF00, y3 = 1.#QNAN0

Подробный вывод выглядит немного странно, но я думаю, что точка понятна.

PS: Кажется, преобразование указателя не требуется. Просто используйте %f, чтобы сообщить функции printf для интерпретации переменной unsigned long long в формате double.

ADD 2

Из любопытства я проверил репрезентацию бит переменных со следующим кодом.

typedef unsigned char *byte_pointer;

void show_bytes(byte_pointer start, int len)
{
    int i;
    for (i = len-1; i>=0; i--)
    {
        printf("%.2x", start[i]);
    }
    printf("\n");
}

И я попробовал код ниже:

//check the infinity and NaN
unsigned long long x1 = 0x7ff0000000000000ULL; // +infinity as double
unsigned long long x2 = 0xfff0000000000000ULL; // -infinity as double
unsigned long long x3 = 0x7ff0000000000001ULL; // NaN as double
double y1 =* ((double *)(&x1));
double y2 =* ((double *)(&x2));
double y3 = *((double *)(&x3));

unsigned long long x4 = x1 + x2;  // I want to check (+infinity)+(-infinity)
double y4 = y1 + y2; // I want to check (+infinity)+(-infinity)

printf("\nx1: ");
show_bytes((byte_pointer)&x1, sizeof(x1));
printf("\nx2: ");
show_bytes((byte_pointer)&x2, sizeof(x2));
printf("\nx3: ");
show_bytes((byte_pointer)&x3, sizeof(x3));
printf("\nx4: ");
show_bytes((byte_pointer)&x4, sizeof(x4));

printf("\ny1: ");
show_bytes((byte_pointer)&y1, sizeof(y1));
printf("\ny2: ");
show_bytes((byte_pointer)&y2, sizeof(y2));
printf("\ny3: ");
show_bytes((byte_pointer)&y3, sizeof(y3));
printf("\ny4: ");
show_bytes((byte_pointer)&y4, sizeof(y4));

Вывод:

x1: 7ff0000000000000

x2: fff0000000000000

x3: 7ff0000000000001

x4: 7fe0000000000000

y1: 7ff0000000000000

y2: fff0000000000000

y3: 7ff8000000000001

y4: fff8000000000000  // <== Different with x4

Странная часть состоит в том, что хотя x1 и x2 имеют одинаковый битовый шаблон как y1 и y2, сумма x4 отличается от y4.

и

printf("\ny4=%f", y4);

дает следующее:

y4=-1.#IND00  // What does it mean???

Почему они разные? И как получается y4?

4b9b3361

Ответ 1

Во-первых, 0x7ff0000000000000 действительно является битовым представлением двойной бесконечности. Но приведение не устанавливает представление битов, оно преобразует логическое значение 0x7ff0000000000000, интерпретируемое как целое число 64 бит. Таким образом, вам нужно использовать другой способ установки битового шаблона.

Прямым способом установки битовой диаграммы будет

uint64_t bits = 0x7ff0000000000000;
double infinity = *(double*)&bits;

Однако это поведение undefined.. Стандарт C запрещает чтение значения, которое было сохранено в качестве одного фундаментального типа (uint64_t) в качестве другого фундаментального типа (double). Это известно как строгие правила псевдонимов и позволяет компилятору генерировать лучший код, потому что он может предположить, что порядок чтения одного типа и запись другого типа не имеет значения.

Единственным исключением из этого правила являются типы char: вам явно разрешено указывать любой указатель на char* и обратно. Поэтому вы можете попробовать использовать этот код:

char bits[] = {0x7f, 0xf0, 0, 0, 0, 0, 0, 0};
double infinity = *(double*)bits;

Несмотря на то, что это не поведение undefined больше, оно по-прежнему зависит от реализации: порядок байтов в double зависит от вашего устройства. Данный код работает на большой конечной машине, такой как ARM и семейство Power, но не на X86. Для X86 вам нужна эта версия:

char bits[] = {0, 0, 0, 0, 0, 0, 0xf0, 0x7f};
double infinity = *(double*)bits;

По-настоящему не существует способа реализации этой реализации, поскольку нет гарантии, что машина будет хранить значения с плавающей запятой в том же порядке, что и целочисленные. Есть даже машины, которые используют байтовые порядки следующим образом: < 1, 0, 3, 2 > Я даже не хочу знать, кто придумал эту блестящую идею, но она существует, и мы должны жить с ней.


К вашему последнему вопросу: арифметика с плавающей запятой по сути отличается от целочисленной арифметики. Биты имеют специальные значения, и блок с плавающей запятой учитывает это. Особенно специальные отношения, такие как бесконечности, NAN и денормализованные числа, рассматриваются особым образом. А так как +inf + -inf определен для получения NAN, ваш блок с плавающей запятой испускает битовый шаблон NAN. Целочисленная единица не знает о бесконечностях или NAN, поэтому она просто интерпретирует битовый шаблон как огромное целое число и с радостью выполняет целочисленное добавление (которое в этом случае происходит с переполнением). Результирующая битовая диаграмма не относится к NAN. Это, скорее всего, битовая диаграмма действительно огромного положительного числа с плавающей запятой (2^1023, если быть точным), но это не имеет никакого значения.


На самом деле существует способ установить битовые шаблоны всех значений, кроме NAN, переносимым образом: учитывая три переменные, содержащие биты знака, экспонента и мантиссы, вы можете сделать это:

uint64_t sign = ..., exponent = ..., mantissa = ...;
double result;
assert(!(exponent == 0x7ff && mantissa));    //Can't set the bits of a NAN in this way.
if(exponent) {
    //This code does not work for denormalized numbers. And it won't honor the value of mantissa when the exponent signals NAN or infinity.
    result = mantissa + (1ull << 52);    //Add the implicit bit.
    result /= (1ull << 52);    //This makes sure that the exponent is logically zero (equals the bias), so that the next operation will work as expected.
    result *= pow(2, (double)((signed)exponent - 0x3ff));    //This sets the exponent.
} else {
    //This code works for denormalized numbers.
    result = mantissa;    //No implicit bit.
    result /= (1ull << 51);    //This ensures that the next operation works as expected.
    result *= pow(2, -0x3ff);    //Scale down to the denormalized range.
}
result *= (sign ? -1.0 : 1.0);    //This sets the sign.

Это использует блок с плавающей запятой, чтобы переместить бит в нужное место. Поскольку нет возможности взаимодействовать с битами мантиссы NAN с использованием арифметики с плавающей запятой, невозможно включить генерацию NAN в этот код. Ну, вы можете создать NAN, но у вас не будет контроля над его битовой диаграммой мантиссы.

Ответ 2

Инициализация

double x1=(double)0x7ff0000000000000;

преобразует литерал целочисленного числа в double. Вероятно, вы захотите поделиться поразрядным представлением. Это специфическое исполнение (возможно неуказанное bahavior), но вы можете использовать union:

union { double x; long long n; } u;
u.n = 0x7ff0000000000000LL;

затем используйте u.x; Я предполагаю, что long long и double являются 64 бит на вашем компьютере. Также важны endianess и с плавающей запятой.

См. также http://floating-point-gui.de/

Обратите внимание, что не все процессоры x86, и не все реализации с плавающей запятой IEEE754 ( даже если в 2014 году большинство из них). Ваш код, вероятно, не будет работать на процессоре ARM, например. в планшете.

Ответ 3

Вы переносите значение в double, и это не будет работать, как вы ожидаете.

double x1=(double)0x7ff0000000000000; // Not setting the value directly

Чтобы избежать этой проблемы, вы можете интерпретировать это значение как двойной указатель и разыменовать его (хотя это ужасно не рекомендуется и будет работать только с unsigned long long == double size):

unsigned long long x1n = 0x7ff0000000000000ULL; // Inf
double x1 = *((double*)&x1n);
unsigned long long x2n = 0x7ff0000000000001ULL; // Signaling NaN
double x2 = *((double*)&x2n);

printf("\nx1=%f, x2=%f sizeof(double) = %d", x1, x2, sizeof(x2));
if (x1 == x2)
    printf("\nx1==x2");
else
    printf("\nx1!=x2"); // x1 != x2

Пример идеона

Ответ 4

Вы преобразовали константу 0x7ff00... в double. Это совсем не то же самое, что взять бит-представление этого значения и интерпретировать его как double.

Это также объясняет, почему x1==x2. Когда вы конвертируете в double, вы теряете точность; поэтому иногда для больших целых чисел double, в результате которого вы входите, одинаково в двух случаях. Это дает вам некоторые странные эффекты, когда для большого значения с плавающей запятой добавление 1 оставляет его неизменным.