浮動小数点数(IEEE754のbinary64形式など、いわゆるfloatやdouble)の表現形式について。
float,doubleだとビット数が多くて全網羅は無理なので、符号1bit、指数部3bit、仮数部2bitで説明する。
概要
浮動小数点数は、$(-1)^f \times m \times 2^n$という形式で数を表現する。
$f$が符号、$m$が仮数部、$n$が指数部にあたる。
指数部
まずは指数部について。
指数部は3bitなので0~7の数値を表せるが、float,doubleでは全ビット0が数値の「$0$」を、全ビット1が「$\infty$」を表すので、指数として使える範囲は1~6。
このままだと小数点以下が表せないので、事前に決めた値を引く。
floatやdoubleだと正の範囲が負の範囲より1だけ大きくなるようにしているので、3を引いて-2~3の範囲で使う。
3bitの指数部により表現できる値は以下の表のとおり。
指数部 | 値 |
---|---|
000 | $0$ |
001 | $2^{-2}$ |
010 | $2^{-1}$ |
011 | $2^0$ |
110 | $2^1$ |
111 | $2^2$ |
110 | $2^3$ |
111 | $\infty$ |
10進で表すと以下のとおり。
指数部 | 値 |
---|---|
000 | $0$ |
001 | $0.25$ |
010 | $0.5$ |
011 | $1$ |
110 | $2$ |
111 | $4$ |
110 | $8$ |
111 | $\infty$ |
表現できる値(4まで)を数直線上にプロットすると、以下のようになる。
+---+---+-------+---------------+-------------------------------+--
| | | | | |
0 0.25 0.5 1 2 4
仮数部
仮数部には、正規化後の値を使用する。
正規化とは、この場合だと「仮数が1以上2未満になるように指数を調整すること」である。
例えば、10進の3.5を素直に2進に変換すると$11.1$となるが、正規化すると$1.11 \times 2^1$となる。
正規化した2進数で仮数を表現すると、小数点の前は1固定となる(数値の「$0$」を表す場合を除く)。
そのため、小数点前の1は省略して、小数点以下のみを仮数部に保存する。
今回は仮数部を2bitとしている。小数点前が1固定なので、表現できる値が2進数で1.00,1.01,1.10,1.11の2^2=4パターン。
10進数にすると1.00,1.25,1.50,1.75となり、1~2の間を4等分した値となる。
指数部・仮数部の組合せにより表現できる値は以下の表のとおり。
指数部\仮数部 | 00 | 01 | 10 | 11 |
---|---|---|---|---|
000 | $0$ | - | - | - |
001 | $1.00 \times 2^{-2}$ | $1.01 \times 2^{-2}$ | $1.10 \times 2^{-2}$ | $1.11 \times 2^{-2}$ |
010 | $1.00 \times 2^{-1}$ | $1.01 \times 2^{-1}$ | $1.10 \times 2^{-1}$ | $1.11 \times 2^{-1}$ |
011 | $1.00 \times 2^0$ | $1.01 \times 2^0$ | $1.10 \times 2^0$ | $1.11 \times 2^0$ |
110 | $1.00 \times 2^1$ | $1.01 \times 2^1$ | $1.10 \times 2^1$ | $1.11 \times 2^1$ |
111 | $1.00 \times 2^2$ | $1.01 \times 2^2$ | $1.10 \times 2^2$ | $1.11 \times 2^2$ |
110 | $1.00 \times 2^3$ | $1.01 \times 2^3$ | $1.10 \times 2^3$ | $1.11 \times 2^3$ |
111 | $\infty$ | - | - | - |
10進で表すと以下のとおり。
指数部\仮数部 | 00 | 01 | 10 | 11 |
---|---|---|---|---|
000 | $0$ | - | - | - |
001 | $0.25$ | $0.3125$ | $0.375$ | $0.4375$ |
010 | $0.5$ | $0.625$ | $0.75$ | $0.875$ |
011 | $1$ | $1.25$ | $1.5$ | $1.75$ |
110 | $2$ | $2.5$ | $3$ | $3.5$ |
111 | $4$ | $5$ | $6$ | $7$ |
110 | $8$ | $10$ | $12$ | $14$ |
111 | $\infty$ | - | - | - |
表現できる値(4まで)を数直線上にプロットすると、以下のようになる。
$2^{n-1}$と$2^n$の間では目盛の間隔が変わらず、$2^n$の境界をまたぐたびに、目盛の間隔が倍になっているのが分かる。
+---+++++-+-+-+-+---+---+---+---+-------+-------+-------+-------+--
| | | | | |
0 0.25 0.5 1 2 4
ちなみに、1と2の間の目盛の間隔が、いわゆる計算機イプシロンに当たる。
今回の表現形式の場合、計算機イプシロンは0.25ということになる。
非正規化数
数直線を見ると分かるが、現状だと0と0.25の間の数を表現できない。
そのため、計算結果が0.125以下だと0に丸められてしまい、割り算で使われると0割りの危険性がある。
例えば、0.3125-0.25=0.0625だが、表現できない数なので0に丸められる。
最小の目盛の間隔が0.0625(0.25と0.5の間)なので、0と0.25の間にも同じ間隔で目盛を振れば、
引き算により0に丸められる事態は避けられる。
0と0.25(2進で$1.00 \times 2^{-2}$)の間に0.0625(2進で$0.01 \times 2^{-2}$)間隔で目盛を振ると、
$0.01 \times 2^{-2}$, $0.10 \times 2^{-2}$, $0.11 \times 2^{-2}$の3つの値となる。
これを指数部0、仮数部1,2,3に割り当てる。
指数部\仮数部 | 00 | 01 | 10 | 11 |
---|---|---|---|---|
000 | $0$ | $0.01 \times 2^{-2}$ | $0.10 \times 2^{-2}$ | $0.11 \times 2^{-2}$ |
001 | $1.00 \times 2^{-2}$ | $1.01 \times 2^{-2}$ | $1.10 \times 2^{-2}$ | $1.11 \times 2^{-2}$ |
010 | $1.00 \times 2^{-1}$ | $1.01 \times 2^{-1}$ | $1.10 \times 2^{-1}$ | $1.11 \times 2^{-1}$ |
011 | $1.00 \times 2^0$ | $1.01 \times 2^0$ | $1.10 \times 2^0$ | $1.11 \times 2^0$ |
110 | $1.00 \times 2^1$ | $1.01 \times 2^1$ | $1.10 \times 2^1$ | $1.11 \times 2^1$ |
111 | $1.00 \times 2^2$ | $1.01 \times 2^2$ | $1.10 \times 2^2$ | $1.11 \times 2^2$ |
110 | $1.00 \times 2^3$ | $1.01 \times 2^3$ | $1.10 \times 2^3$ | $1.11 \times 2^3$ |
111 | $\infty$ | - | - | - |
小数点前が1ではない(正規化されていない)ということで、非正規化数と呼ばれる。
10進で表すと以下のとおり。
指数部\仮数部 | 00 | 01 | 10 | 11 |
---|---|---|---|---|
000 | $0$ | $0.0625$ | $0.125$ | $0.1875$ |
001 | $0.25$ | $0.3125$ | $0.375$ | $0.4375$ |
010 | $0.5$ | $0.625$ | $0.75$ | $0.875$ |
011 | $1$ | $1.25$ | $1.5$ | $1.75$ |
110 | $2$ | $2.5$ | $3$ | $3.5$ |
111 | $4$ | $5$ | $6$ | $7$ |
110 | $8$ | $10$ | $12$ | $14$ |
111 | $\infty$ | - | - | - |
表現できる値(4まで)を数直線上にプロットすると、以下のようになる。
+++++++++-+-+-+-+---+---+---+---+-------+-------+-------+-------+--
| | | | | |
0 0.25 0.5 1 2 4
NaN
NaNは、0/0などの不正演算の結果を表す値。
指数部7、仮数部0以外の部分に割り当てられる。
NaNには、演算に使っても例外発生しないquiet NaNと、例外発生するsignaling NaNの二種類がある。
float,doubleの場合、仮数部先頭ビットが1だとquiet NaN、0だとsignaling NaNになる。
指数部\仮数部 | 00 | 01 | 10 | 11 |
---|---|---|---|---|
000 | $0$ | $0.0625$ | $0.125$ | $0.1875$ |
001 | $0.25$ | $0.3125$ | $0.375$ | $0.4375$ |
010 | $0.5$ | $0.625$ | $0.75$ | $0.875$ |
011 | $1$ | $1.25$ | $1.5$ | $1.75$ |
110 | $2$ | $2.5$ | $3$ | $3.5$ |
111 | $4$ | $5$ | $6$ | $7$ |
110 | $8$ | $10$ | $12$ | $14$ |
111 | $\infty$ | sNaN | qNaN | qNaN |
符号
符号ビットは、0なら正、1なら負。
特筆すべきは、+0と-0、+NaNと-NaNが別個に存在すること。
基本的に符号の差は無視されるが、-0を文字列化すると符号が付くなど差が出る場合もあるので注意。
実装
C++でdoubleとの相互変換を実装。
C/C++でこの手の処理を実装すると、Strict Aliasing Rulesに引っ掛かりやすいので注意。
# include <cstdint>
# include <cmath>
# include <cstring>
double MyFloat2Dbl(uint8_t bits) {
int exponent = static_cast<int>((bits >> 2) & 0x7) - 3;
uint64_t fraction = bits & 0x3;
uint64_t dsign = bits >> 5;
int dexponent;
// MyFloatの無限大 or NaN
if (exponent == 4)
dexponent = 0x400;
// MyFloatの正規化数
else if (exponent > -3)
dexponent = exponent;
// MyFloatの非正規化数
else if (fraction > 0) {
// 正規化数に変換
dexponent = -3;
fraction <<= 1;
while (!(fraction & 0x4)) {
--dexponent;
fraction <<= 1;
}
fraction &= 0x3;
}
// MyFloatのゼロ
else
dexponent = -0x3ff;
uint64_t dfraction = fraction << 50;
uint64_t dbits = (dsign << 63) | (static_cast<uint64_t>(dexponent + 0x3ff) << 52) | dfraction;
double d;
std::memcpy(&d, &dbits, sizeof(dbits));
return d;
}
uint8_t Dbl2MyFloat(double d) {
uint8_t sign = std::signbit(d) ? 1 : 0, fraction;
int exponent;
if (std::isfinite(d)) {
// doubleをMyFloatの有効桁で事前に丸めておく(丸めた結果範囲外になる場合への対応)
int dexponent;
double dfr = std::frexp(d, &dexponent);
if (dexponent >= -1) {
// MyFloat正規化数の範囲なので3桁(仮数部2桁、dfrが[0.5,1)なので+1桁)
dfr = std::nearbyint(dfr * 8) / 8;
}
else {
// MyFloat非正規化数の範囲なので、元の数をMyFloat非正規化数最小の桁で丸め
dfr = std::nearbyint(d * 16) / 16;
dexponent = 0;
}
// 丸めた結果込みのdoubleの情報取得
uint64_t dbits;
std::memcpy(&dbits, &dfr, sizeof(dfr));
dexponent += static_cast<int>((dbits >> 52) & 0x7ff) - 0x3ff;
uint64_t dfraction = dbits & 0xf'ffff'ffff'ffff;
// MyFloatで表現可能な最大値より大きい(無限大にする)
if (dexponent > 3) {
fraction = 0;
exponent = 4;
}
// MyFloatの正規化数の範囲
else if (dexponent >= -2) {
fraction = static_cast<uint8_t>(dfraction >> 50);
exponent = dexponent;
}
// MyFloatの非正規化数の範囲
else if (dexponent >= -4) {
// けち表現で削ったビットを補ったうえでずらして非正規化数にする
fraction = (static_cast<uint8_t>(dfraction >> 51) | 0x2) >> (-dexponent - 3);
exponent = -3;
}
// MyFloatで表現可能な正の最小値より小さい(0にする)
// doubleのゼロ、非正規化数も結果的にこちらで処理
else {
fraction = 0;
exponent = -3;
}
}
// doubleの無限大 or NaN
else {
uint64_t dbits;
std::memcpy(&dbits, &d, sizeof(d));
uint64_t dfraction = dbits & 0xf'ffff'ffff'ffff;
bool is_sNaN = (dfraction != 0) && !(dfraction >> 51);
fraction = static_cast<uint8_t>(dfraction >> 50);
// sNaNのfractionは!=0でなければならないので、0になっていたら1を入れる
if (is_sNaN && fraction == 0)
fraction = 0x1;
exponent = 4;
}
return (sign << 5) | (static_cast<uint8_t>(exponent + 3) << 2) | fraction;
}