まず、浮動小数点の弱点をよりよく理解したい場合は、読むことを検討すべき論文: "What Every Computer Scientist Should Know About Floating Point Arithmetic" http://www.validlab.com/goldberg/paper.pdf
そして今、いくつかの肉に。
unsigned int
次のコードは基本的なもので、範囲 0 < 値 < 2 24から IEEE-754 単精度浮動小数点数を生成しようとします。これは、最新のハードウェアで最も遭遇する可能性が高い形式であり、元の質問で参照しているように見える形式です。
IEEE-754 単精度浮動小数点数は、1 つの符号ビット、8 ビットの指数、および 23 ビットの仮数 (仮数とも呼ばれる) の 3 つのフィールドに分割されます。IEEE-754 は隠し 1仮数部を使用します。これは仮数部が実際には合計 24 ビットであることを意味します。ビットは左から右にパックされ、ビット 31 に符号ビット、ビット 30 .. 23 に指数、ビット 22 .. 0 に仮数が含まれます。ウィキペディアの次の図は、次のことを示しています。
指数には 127 のバイアスがあります。つまり、浮動小数点数に関連付けられた実際の指数は、指数フィールドに格納されている値よりも 127 小さいことを意味します。したがって、0 の指数は 127 としてエンコードされます。
(注: ウィキペディアの完全な記事は興味深いかもしれません。参照: http://en.wikipedia.org/wiki/Single_precision_floating-point_format )
したがって、IEEE-754 番号 0x40000000 は次のように解釈されます。
- ビット 31 = 0: 正の値
- ビット 30 .. 23 = 0x80: 指数 = 128 - 127 = 1 (aka. 2 1 )
- ビット 22 .. 0 はすべて 0 です: 仮数 = 1.00000000_00000000_0000000. (非表示の1を復元したことに注意してください)。
したがって、値は 1.0 x 2 1 = 2.0 です。
上記の限られた範囲の を IEEE-754 形式に変換するunsigned int
には、次のような関数を使用できます。次の手順を実行します。
- 整数の先頭の 1 を浮動小数点表現の非表示の 1 の位置に揃えます。
- 整数を整列させながら、行われたシフトの総数を記録します。
- 非表示の 1 をマスクします。
- 行われたシフトの数を使用して、指数を計算し、それを数値に追加します。
- を使用し
reinterpret_cast
て、結果のビットパターンを に変換しfloat
ます。この部分は、型打ちされたポインターを使用しているため、醜いハックです。を悪用してこれを行うこともできますunion
。_itof
一部のプラットフォームでは、この再解釈の見苦しさを軽減するための組み込み操作 ( など) が提供されています。
これを行うには、はるかに高速な方法があります。これは、非常に効率的ではないにしても、教育的に役立つことを意図しています。
float uint_to_float(unsigned int significand)
{
// Only support 0 < significand < 1 << 24.
if (significand == 0 || significand >= 1 << 24)
return -1.0; // or abort(); or whatever you'd like here.
int shifts = 0;
// Align the leading 1 of the significand to the hidden-1
// position. Count the number of shifts required.
while ((significand & (1 << 23)) == 0)
{
significand <<= 1;
shifts++;
}
// The number 1.0 has an exponent of 0, and would need to be
// shifted left 23 times. The number 2.0, however, has an
// exponent of 1 and needs to be shifted left only 22 times.
// Therefore, the exponent should be (23 - shifts). IEEE-754
// format requires a bias of 127, though, so the exponent field
// is given by the following expression:
unsigned int exponent = 127 + 23 - shifts;
// Now merge significand and exponent. Be sure to strip away
// the hidden 1 in the significand.
unsigned int merged = (exponent << 23) | (significand & 0x7FFFFF);
// Reinterpret as a float and return. This is an evil hack.
return *reinterpret_cast< float* >( &merged );
}
数値の先頭の 1 を検出する関数を使用すると、このプロセスをより効率的に行うことができます。clz
(これらは、「先行ゼロのカウント」や「正規化」などの名前で呼ばれることがありますnorm
。)
符号を記録し、整数の絶対値を取得し、上記の手順を実行してから、符号を数値のビット 31 に挿入することで、これを符号付き数値に拡張することもできます。
整数 >= 2 24の場合、整数全体が 32 ビット浮動小数点形式の仮数フィールドに収まりません。これが、「丸める」必要がある理由です。値を適合させるために、LSB を失います。したがって、複数の整数が同じ浮動小数点パターンにマッピングされることになります。正確なマッピングは、丸めモード (-Inf 方向への丸め、+Inf 方向への丸め、ゼロ方向への丸め、最も近い偶数方向への丸め) によって異なります。しかし、問題は、24 ビットを 24 ビット未満に押し込むことはできず、損失が発生するということです。
これは、上記のコードで確認できます。先頭の 1 を非表示の 1 の位置に揃えることで機能します。値が >= 2 24の場合、コードはleftではなくrightにシフトする必要があり、必然的に LSB が離れてシフトされます。丸めモードは、シフトされたビットを処理する方法を示しているだけです。