2009年11月06日 (金)
浮動小数点(IEEE 754, 32bit 単精度)について
- ループの終了条件に小数を指定するのは避けるべき [王様の箱庭]
- 私もうfloatくんのこと…信じられない…! [王様の箱庭]
を受けて書いてみた.
C 言語の float は IEEE 754 準拠だと明記されていないのですが,基本的には IEEE 754 形式だと思っていいはずなので,IEEE 754 基準で調べたら OK です.
IEEE 754 では,32bit の 2 進数で小数を表現するために,内部を「符号,指数部,仮数部」に分けています.それぞれが「1bit, 8bit, 23bit」です.
基本的に IEEE 754 - Wikipedia などを読めば分かるのですが,理解の確認のために再整理.詳しいことはパタヘネ辺りを読めばいいと思います.後で参照して補足します.
2進数は,小数点でどうやって表現されているか
浮動小数点 と 固定小数点 の2通りがありますが,ここでは浮動小数点のみを扱います.なお,固定小数点は,ある桁に小数点があるものと見なす(例えば,256 倍しておく)ものです.
32bit の 浮動小数点数 は,基本的には,以下の数式で表現できます( ^ は累乗).この形式で表現できるのは正規化数です.例外は後述.
(-1) ^ (符号) * (1.仮数部) * 2 ^ (指数部-127)
符号部
これは単純に 0 なら 正,1 なら 負 です.整数の 2 の補数表現とあわされています.
仮数部
小数を IEEE 754 形式に表現し直す際に, 1.XXXX * 2^exp という形に直します.これを正規化といいます.仮数部には 1. の下の小数部である XXXX のみを持ちます.
は説明がわかりにくいですが,2.5 であれば(十進数を xxx (10),二進数を xxx (2) と表すこととすると),
2.5 (10) = 10.1 (2) * 2^0 = 1.01 (2) * 2^1
と正規化され 0.25 (10) = 0.01 (2) となりますので,仮数部は 01 となります.残りの 21 bit は 0 で埋めます.
指数部
指数部は 「指数 + 127」 の値を格納します.なぜ,2 の補数を使わないかというと,(正規化数の)浮動小数点数の大小比較を簡単にするためです(混乱中ですが,たぶん後述).すなわち,8bit の 0 〜 255 で -127〜128 を表現します.先ほどと同様に,Wikipedia の例をパクってくると, 2^1 は指数部に 128 として表現されます.
よって,2.5 (10) = (-1)^0 * 1.01(2) * 2^1 は 0_1000,0000_0100,0000,0000,0000,0000,000 として,表現されます(コンマは手前から 4 桁区切りでつけているだけです).
irb で確認
「packテンプレート文字列 - Rubyリファレンスマニュアル」によると,IEEE 754 準拠な環境なら簡単に変換結果を確認できるようです.
> [2.5].pack("g").unpack("B*")[0][0,32]
=> "01000000001000000000000000000000"
# 符号
> [2.5].pack("g").unpack("B*")[0][0,1]
=> "0"
# 指数部
> [2.5].pack("g").unpack("B*")[0][1,8]
=> "10000000"
# 仮数部
> [2.5].pack("g").unpack("B*")[0][9,23]
=> "01000000000000000000000"
確かにうまくいっています.
16777216.0, 16777217.0, 16777215.0 はどうなるのか?
16777216.0 (10) = 1.0 (2) * 2^24 ですので,符号は 0,指数部は 24+127,仮数部は 0 となりますね.
一方,16777217.0 (10) = (1.0 + 2^(-24) ) (2) * 2^24 ですので,2^(-24) を表現するには仮数部が 1 bit 足りず(23bit では 2^(-1) 〜 2^(-23) まで),丸められることとなります.
16777218.0 (10) だと (1.0 + 2^(-23) ) (2) * 2^24 ですので,23bit にぎりぎり収まるため,float で表現可能になります.
irb で確認してみると,確かに,16777216.0 == 16777217.0 != 16777218.0 となっていますね.
> [16777216].pack("g").unpack("B*")[0][0,32] # 仮数部は 00000000000000000000000
=> "01001011100000000000000000000000"
> [16777217].pack("g").unpack("B*")[0][0,32] # 仮数部は 00000000000000000000000
=> "01001011100000000000000000000000"
> [16777218].pack("g").unpack("B*")[0][0,32] # 仮数部は 00000000000000000000001
=> "01001011100000000000000000000001"
すなわち,float だと 16777216 = 2^24 以上では,2.0 未満の違いを表現できなくなります不正確なので削除.double だと,仮数部の bit 数が増えるので,今回の数は全て表現可能です.
16777215.0 になると,今度は指数部が 1 小さくなりますので,仮数部において 1.0 が 2^(-23) * 2^23 と表現できるようになり,丸めが発生しません.
# 1 違いが大きな違いに
> [16777215].pack("g").unpack("B*")[0][0,32]
=> "01001011011111111111111111111111"
> [16777216].pack("g").unpack("B*")[0][0,32]
=> "01001011100000000000000000000000"
# 16777215 と 16777216 との指数部の比較をしてみると,1 異なる
> [16777215].pack("g").unpack("B*")[0][1,8]
=> "10010110"
> [16777216].pack("g").unpack("B*")[0][1,8]
=> "10010111"
0.1 について
0.1 というのは,2進数の浮動小数点表記において,正確に表現することができないかずです.直感的には,そんな馬鹿な!と言いそうになるのですが, 2^n をどう足し合わせても 0.1 ちょっきりにはなりません(0.1 = 1 / 2 * 1 / 5 ですが,5 は素数のため,1/5 は m/2^n では表すことができません).
では 0.1 がどう表現されるかというと,正規化すると 16 倍して 1 を超えるので,指数部は -4 となり
0.1 * 16 (10) = 1.6 * 2^(-4) (10)
仮数部は 0.6 ≒ 1/2 + 1/16 + 1/32 + ... (= 0.5 + 0.0625 + 0.03125 + ...) であり,実際に irb で確認してみると(厳密には 0.1 0110 0110 0110 ... と 0110 が繰り返す循環小数となるはずです)
> [0.1].pack("g").unpack("B*")[0][0,1]
=> "0"
> [0.1].pack("g").unpack("B*")[0][1,8]
=> "01111011"
> [0.1].pack("g").unpack("B*")[0][9,23]
=> "10011001100110011001101"
こちらは,bit 数不足の問題ではないので,double でも回避できない本質的な表現能力の問題です.3進数では正確に表現できる 1/3 を 10進数 では循環小数としてしか表現できないのと同じことです.
後,たぶん,一般にループの終端条件に小数を用いる場合は許容誤差込みで大小判定します.while (f != 10.0) ではなく while (10.0 - error <= f && f <= 10.0 + error) のようにするのではないかと思います.たぶんね.
非正規化数について
余力が有れば追記.
大小比較について(整理中)
というのも,正規化されていれば, (-1)^符号 * 1.仮数部 * 2^(指数部+127) と表現されますので,「符号,指数部,仮数部」と並べておくことで,大小比較が簡単になるはずです.
例えば,lhs と rhs の大小を比べる際には,
lhs, rhs の双方が正
- どちらかの指数部が大きい → 指数部の大きい数が大きい(∵正規化されているので)
- 指数部が等しい → 仮数部のより大きい数が大きい
これは,正の整数同士の比較において,上位桁が大きいほど大きいということで正しく判断できる.
lhs, rhs のいずれかが負
- 2の補数表現では最上位ビットが 1 だと負であり,「正の数 > 負の数」として正しく判断できる.
lhs, rhs の双方が負
- どちらかの指数部が大きい → 指数部の大きい数が小さい
- 指数部が等しい → 仮数部のより大きい数が大きい
こちらは 2 の補数表現同士の比較と一致しない・・・でいいんだっけ?

