IEEE の単精度浮動小数点数規格では、32ビット(4バイト)のうち最上位ビットから
- 1 ビットの符号ビット(0 が正、1 が負)
- 8 ビットの指数部(バイアスが 127)
- 23 ビットの仮数部
となっている。したがって、符号ビットを s, 指数部を e, 仮数部(のビットパターン)を f とすると、正規化されたものなら、
(-1)^s x 2^(e - 127) x 1.f
1 <= e <= 254 ならば正規化されている。e = 0 かつ f = 0 ならゼロを表すが符号ビットによって、通常のゼロか 負のゼロ かが決まる。
e = 0 かつ f != 0 の場合は正規化されていないが有効な値だ。非正規化数の場合は
(-1)^s x 2^(1-127) x 0.f
となる。指数が 0 - 127 = -127 ではなくて 1 - 127 = -126 になることに注意。
e = 255 の場合は f により無限大だったり NaN になる。
このあたりのことを C コード float_check.c で確認した。出力は次のようになる。
normalized_max_f = 340282346638528859811704183484516925440.00000000000000000000
normalized_min_f = -340282346638528859811704183484516925440.00000000000000000000
zero_f = 0.00000000000000000000
negative_zero_f = -0.00000000000000000000
unnormalized_f = 0.00000000000000000000000000000000000000000000140130
positive_infinite_f = inf
nan_f = nanしたがって、正規化数の(絶対値が)最小数は、指数部のビットパターンは最下位ビットのみ 1 で仮数の 23 ビットがすべて 0 であるから次の値であり、
1.00000000000000000000000 x 2^(-126)
一方、非正規化数の最小数は、指数部のビットパターンはすべて 0 で仮数部の 23 ビットは最下位ビットのみが 1 であるから次の値になる。
0.00000000000000000000001 x 2^(-126)
になる。非正規化数は正規化数の最小値よりも小さく、0 よりは大きいということになる。
仮数部が 23 ビットであり、これに 1 を足した 24 ビットが単精度浮動小数点数が保持できる 2 進での桁数である。ほぼ 2^24 = 10^7 だから、10進数なら 7 桁の精度を持つことになる。
したがって、8 桁の10進数は単精度浮動小数点数ではうまく表現できない場合がある。たとえば、
>>> 2**24
16777216であり、この8桁の10進数は25ビットの2進数 1000000000000000000000000 に等しい。これは
2^24 x 1.000000000000000000000000(0 は24個)
であり、単精度浮動小数点数で表現すると、
- 符号ビットは
s = 0 - 指数部は、
e = 24 + 127 = 151 - 仮数部は 23ビットの 0
である。実際、10進数 16777216 を単精度浮動小数点数にし、それのビットパターンを表示させるために次の Python コードを実行すると、
import numpy as np
import struct
x = np.float32(16777216)
# np.float32 -> bytes -> int
bytes_rep = struct.pack('>f', x)
int_rep = struct.unpack('>I', bytes_rep)[0]
# int -> binary string
bin_rep = format(int_rep, '032b')
print(f"Floating point value: {x}")
print(f"Binary representation: {bin_rep}")次のような出力が得られる。
Floating point value: 16777216.0
Binary representation: 01001011100000000000000000000000指数部の 10010111 は10進数の 151 であり、確かに合っている。
一方で 16777217 = 16777216 + 1 は 25 ビットの2進数 1000000000000000000000001 に等しい。これは
2^24 x 1.000000000000000000000001(小数点以下は 24個ある)
であり、単精度浮動小数点数にすると、仮数部が 23 ビットしかないので、16777216 と同じ表現になってしまう。実際、
import numpy as np
import struct
x = np.float32(16777217)
# np.float32 -> bytes -> int
bytes_rep = struct.pack('>f', x)
int_rep = struct.unpack('>I', bytes_rep)[0]
# int -> binary string
bin_rep = format(int_rep, '032b')
print(f"Floating point value: {x}")
print(f"Binary representation: {bin_rep}")の出力は、
Floating point value: 16777216.0
Binary representation: 01001011100000000000000000000000のように前と全く同じになってしまうのだ。
16777217 はうまく表せないが、16777218 は正確に表現できる。これは、25 ビットの2進数 1000000000000000000000010 に等しいので、仮数部の 23 ビットにうまく収まるからだ。
整数であれば問題ないが、小数の場合、10進数では有限小数でも 2 進数だと循環小数になってしまうことがある。たとえば、10 進数の 0.1 は 2 進数だと
0.00011001100110011....
のように 0011 が無限に続いていく。これは、1.100110011001100110011..... x 2^(-4) だから、単精度浮動小数点数では
- 符号ビット
s = 0 - 指数部は
e = 127 - 4 = 123 - 仮数部は
f = 10011001100110011001101
になる。f の最後のビットは 0 になるはずだが、次のビットが 1 なので、それを切り上げている。実際、
import numpy as np
import struct
x = np.float32(0.1)
# np.float32 -> bytes -> int
bytes_rep = struct.pack('>f', x)
int_rep = struct.unpack('>I', bytes_rep)[0]
# int -> binary string
bin_rep = format(int_rep, '032b')
print(f"Floating point value: {x}")
print(f"Binary representation: {bin_rep}")の出力は、
Floating point value: 0.10000000149011612
Binary representation: 00111101110011001100110011001101となる。0.10000000149011612 が 10 進数表記だが、切り上げているので、その分、値が大きくなってしまっているのだ。