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

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

  • s: 1 бит для знака
  • exp: 11 бит для экспоненты
  • Фракция: 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.)

ДОБАВИТЬ 1

Я изменил код, как показано ниже, и успешно подтвердил Infinity и 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 формате.

ДОБАВИТЬ 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?

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

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

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

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

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

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

Несмотря на то, что это не является неопределенным поведением, это все еще определенное поведение реализации : порядок байтов в 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, но у вас не будет контроля над его битовой диаграммой мантиссы.

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

 double x1=(double)0x7ff0000000000000; 

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

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

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

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

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

Вы меняете значение на двойной, и это не будет работать так, как вы ожидаете.

 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 

Пример по идеону

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

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