一から勉強させてください( ̄ω ̄;)

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

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

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

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

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

参考