一から勉強させてください

最下級エンジニアが日々の学びをアウトプットしていくだけのブログです

浮動小数点数について本気出して考えてみた

(2016/12/30 追記)コメントでご指摘頂いた箇所に関して修正しました。


この前たまたまこちらの記事を見つけて、浮動小数点数について全然理解してないなって思ったので勉強してみました。

前提

コンピュータの中に保存されているデータはすべて 0 と 1 の集まり(ビット列)で表現されている。そしてどういうビット列をどういう値として解釈するかは特に決まっておらず、使う人が勝手に決めていい。でもそれだと混乱を招くのでルールを作る必要がある。ルールは人間が勝手に決めた約束事に過ぎない。

固定小数点数浮動小数点数

コンピュータは 0 と 1 しか表現できないから小数点.も表現できない。よって小数点付きの実数を表すときのルールが必要となり、そのルールとして固定小数点数浮動小数点数がある。

固定小数点数

ビット列のどこに小数点をつけるか、あらかじめピンポイントで決めてしまう。

例えば「下 1 桁を小数部としましょう」というルールを事前に定めておく。すると 1111 は 111.1 と考えることができ、10 進数では

(2^2 * 1) + (2^1 * 1) +  (2^0 * 1) + (1/2 * 1)

で、7.5 になる。

扱える値の範囲は狭いが、計算が整数演算でできるため計算コストが低い等の利点がある。

浮動小数点数

コンピュータが扱ってるのはこちらの形式。

指数形式(◯×△ の □ 乗)で値を表現する。例えば 1230 は 1.23×103 みたいな。この時、1.23 を仮数、10 を基数、3 を指数という。 これを基数を 2, 仮数、指数を 2 進数で表現するのが浮動小数点数

扱える値の範囲が広い。

IEEE754 規格

浮動小数点数にはいくつか種類があり、昔はコンピュータごとに処理の仕方が違うようなカオスな状況だったらしい。 で、それではいかんと共通の取り決めを定めたのが IEEE754 という偉い人たちの集まり。その時決まったルールが IEEE754 規格。これが現在、浮動小数点数の代表的な規格なのでこれを抑えておけばとりあえず問題ないっぽい。

状況によって求められる精度が違うことを考慮して、32 桁バージョン(単精度)と 64 桁バージョン(倍精度)を用意してくれている。

IEEE754(単精度)を使ってみる

32 ビットを

符号(1 ビット) | 指数部(8 ビット) | 仮数部(23 ビット)

に分けて、それぞれ以下のようにする。

  • 符号: 正の値なら 0, 負の値なら 1。

  • 指数: 実際の値に 127 を加えた値を使う。8 桁なのでほんとは 0~255 を表現できるが指数部が負の時にも良い感じに対応したいから、-127~128 を表現して 127 を足すようにしてる。127 をバイアス値というらしい。

  • 仮数: 指数を調整して 1.◯◯ のように 1.で始まるように小数点を移動する。先頭の 1 は絶対なので無視する。

実際に 10 進数の小数を IEEE754 の単精度で表現してみる。

例えば 7.25 の場合。

符号は正の値なので 0 で決定。

次にこれを 2 進数にする。7.25 は

7 + 0.25

に分解して考える。

まず 7 をひたすら 2 で割っていった余りを調べる。

7 / 2 → 3 余り 1
3 / 2 → 1 余り 1
1 / 2 → 0 余り 1

よって 7 は 2 進数で

111

次に 0.25 にひたすら 2 をかけていった値が 1 以上か調べる。

0.25 * 2 → 0.5 1未満なので0
0.5 * 2 → 1.0 1以上なので1

0.25 は 2 進数で

0.01

2 つを足して 7.25 は

111.01

となる。

これを 1.◯◯×2^△ の形式になるようにすると、

111.01 → 1.1101 * 2^2

仮数 1.1101 の 1.の部分は絶対なので無視。そして残りを 23 ビットになるように 0 でうめると

11010000000000000000000

指数は 2 だったのでバイアス値 127 を足すと、

2 + 127 → 129

これを 2 進数になおす。

129 / 2 → 64 余り 1
64 / 2 → 32 余り 0
32 / 2 → 16 余り 0
16 / 2 → 8 余り 0
8 / 2 → 4 余り 0
4 / 2 → 2 余り 0
2 / 2 → 1 余り 0
1 / 2 → 0 余り 1

よって

10000001

以上から 7.25 を浮動小数点数の単精度で求めると符号、指数、仮数を合わせて

01000000111010000000000000000000

となる。

ちなみに倍精度の場合は指数部が 11 ビット、仮数部が 52 ビット、バイアス値は 1023 になる。それ以外の計算方法は基本同じだと思う。

IEEE754(単精度)で入りきらない値を丸める

次に先ほどと同じ方法で 0.1 を表現しようとすると、

0.1 は 2 進数で

0.1 * 2 → 0.2 1未満なので0
0.2 * 2 → 0.4 1未満なので0
0.4 * 2 → 0.8 1未満なので0
0.8 * 2 → 1.6 1以上なので1
0.6 * 2 → 1.2 1以上なので1
0.2 * 2 → 0.4 1未満なので0
0.4 * 2 → 0.8 1未満なので0
0.8 * 2 → 1.6 1以上なので1
0.6 * 2 → 1.2 1以上なので1
0.2 * 2 → 0.4 1未満なので0
0.4 * 2 → 0.8 1未満なので0
.
.
.

循環小数になる/(^o^)\

0.00011001100…

ここでめげずに先ほどと同じく単精度で表現してみる。

1.1001100… * 2^-4

であることから仮数部は

100110011001100…

指数部は

-4 + 127 → 123

なので、

01111011

よって

00111101110011001100110011001100…

このように入りきらなくなるので値を丸める必要がある。

IEEE754 では丸め方はいろいろ種類があるみたいだけど、デフォルトは最近接偶数丸め。0 は切り捨てて 1 は切り上げる。 今回の場合だと

00111101110011001100110011001100|1

1 なので切り上げる。

00111101110011001100110011001101

でけた。

まとめ

まだ理解は浅いですが、ざっと浮動小数点数についてみてきました。

例えば JavaScript では小数は浮動小数点数の倍精度(64bit のほう。double 型)で表現しますが、実際にブラウザのコンソールで0.1 + 0.2を実行してみると

0.1 + 0.2 = 0.30000000000000004

となり、0.3 にはなりません。 明らかに誤差が生じていますが、これも浮動小数点数の性質を考えれば仕方がないかと納得いきます。

このような性質をしっかり理解して変なとこでハマらないようにしたいもんです。

何か間違った箇所などあればご指摘いただけると幸いです。

参考