Возможно (по крайней мере для значений IEEE 754 float
и double
) вычислить наибольшее значение с плавающей запятой с помощью (псевдокода):
~(-1.0) | 0.5
Прежде чем мы сможем выполнить битовый твидлинг, нам нужно будет преобразовать значения с плавающей точкой в целые числа, а затем обратно. Это можно сделать следующим образом:
uint64_t m_one, half;
double max;
*(double *)(void *)&m_one = -1.0;
*(double *)(void *)&half = 0.5;
*(uint64_t *)(void *)&max = ~m_one | half;
Так как это работает? Для этого нам нужно знать, как будут кодироваться значения с плавающей точкой.
Старший бит кодирует знак, следующие k
биты кодируют показатель степени, а младшие биты будут содержать дробную часть. Для степеней 2
дробная часть равна 0
.
Показатель степени будет сохранен со смещением (смещением) 2**(k-1) - 1
, что означает, что показатель 0
соответствует шаблону со всеми установленными битами, кроме самого старшего.
Существуют две битовые комбинации экспоненты со специальным значением:
- если бит не установлен, значение будет
0
, если дробная часть равна нулю; в противном случае значение является ненормальным
- если установлены все биты, значение равно
infinity
или NaN
Это означает, что наибольший регулярный показатель будет закодирован путем установки всех битов, кроме младшего, что соответствует значению 2**k - 2
или 2**(k-1) - 1
, если вы вычесть смещение.
Для значений double
, k = 11
, то есть максимальный показатель будет равен 1023
, поэтому наибольшее значение с плавающей запятой имеет порядок 2**1023
, что составляет около 1E+308
.
Наибольшее значение будет иметь
- бит знака установлен на
0
- все биты, кроме младшего показателя степени, установлены на
1
- все дробные биты установлены в
1
Теперь можно понять, как работают наши магические числа:
-1.0
имеет свой установленный бит знака, экспонента - это смещение - т.е. присутствуют все биты, кроме старшего - и дробная часть равна 0
~(-1.0)
имеет только самый большой бит экспоненты и все дробные биты установлены
0.5
имеет знаковый бит и дробную часть 0
; экспонента будет смещением минус 1
, т.е. будут присутствовать все, кроме старшего и младшего битов экспоненты
Когда мы объединяем эти два значения с помощью логического или, мы получим требуемый битовый шаблон.
Вычисление также работает для 80-битных значений повышенной точности x86 (также известных как long double
), но битовое перемешивание должно выполняться побайтово, поскольку нет целочисленного типа, достаточно большого, чтобы хранить значения на 32-битном оборудовании .
На самом деле смещение не обязательно должно быть 2**(k-1) - 1
- оно будет работать при произвольном смещении, пока оно нечетное. Смещение должно быть нечетным, потому что в противном случае битовые комбинации для показателя степени 1.0
и 0.5
будут отличаться в других местах, чем младший бит.
Если базовое значение b
(он же radix) типа с плавающей запятой не равно 2
, вам нужно использовать b**(-1)
вместо 0.5 = 2**(-1)
.
Если наибольшее значение показателя не зарезервировано, используйте 1.0
вместо 0.5
. Это будет работать независимо от базы или смещения (то есть больше не ограничено нечетными значениями). Разница в использовании 1.0
заключается в том, что младший бит экспоненты не будет очищен.
Подведем итог:
~(-1.0) | 0.5
работает до тех пор, пока основание равно 2
, смещение нечетное и максимальный показатель зарезервирован.
~(-1.0) | 1.0
работает для любого радиуса или смещения до тех пор, пока максимальный показатель не зарезервирован.