Как бесконечность представлена в 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?
Ответы
Ответ 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 оставляет его неизменным.