HOME » » » » HERE

コラム_浮動小数点数の限界を知る!(この画像は、https://www.photo-ac.com/さまから借用しています)

こんにちわ、横浜すみっこクリエイターととです。

ゲーム制作をする中で浮動小数点数について、悩み倒す機会がありました。そこで今回は、閑話休題、コラムというカタチで綴ってみたいと思います。

軽い読み物程度~、と思って筆を執りましたが、コラムとしてはなかなかの超大作になってしまいました。。。

最後までお付き合いいただければ幸いです。

はじめに

Unityで広いマップを作ってみようと調べてみると、「どうも、floatの限界を考える必要があるようだ」ということに行きつきました。

いろいろな方が解説をされておられますが、基礎知識のない私の頭では、なかなか理解が追い付きません。

いっそのこと、とことん細かく整理しながら自分なりに考え直してみることにしました。

結論

先に結論を書いておきます。

結論:「floatは、10進数で7桁程度までの精度しか担保できない」

例えば、Unityでは、Transformの基準単位1 = 1.0fになっています。基準単位1 = 1メートルとしたとき、原点から10キロメートル離れた位置では、1ミリメートル以下の足し算がうまく処理できなくなっています。

では、どうして7桁あたりに限界があるのか、順を追ってみていきましょう!

浮動小数点数とは

まずは、「float」つまり浮動小数点数について確認しましょう。

浮動小数点数は、

基準となる数に、倍率を掛けて数字を表現しよう

というものです。

小難しい感じがしますが、この考え方はだいぶ日常生活に根付いています。例えば、重さで考えると次のようになりますよね。

重さの倍率表現
倍率 単位 累乗表現
1/1000 ミリグラム(mg) x 10-3
基準(1倍) グラム(g) x 100
1000倍 キログラム(kg) x 103
1000000倍 トン(t) x 106

1.8 kgは、1.8 × 103と表現できます。

数式で表現してみましょう。基準となる数F指数Eとすると、次のようになります。

± F × 10E

これを「2進数でやってやろうぜ」というのが、プログラミングでの浮動小数点数になります。つまり、

± F × 2E

これが2進数の浮動小数点数を表現する式になります。詳細は後述しますが、Fは1以上2未満の値を取ります。

上の式で「F = 1」として、Eの値を変えてみると「浮動」と呼ばれる所以が見えてきます。

2進数の浮動少数点
E 2進数
-15 0.000000000000001
-10 0.000000000100000
-5 0.000010000000000
-2 0.010000000000000
-1 0.100000000000000
0 1.000000000000000
1 10.000000000000000
2 100.000000000000000
5 100000.000000000000000
10 10000000000.000000000000000
15 1000000000000000.000000000000000

当たり前ですが、Eを変えていくと小数点の位置が左右にシフトしています。小数点がふわふわ動く = 浮動(フロート)するので、「浮動小数点数(float)」と呼ばれているのです。

浮動小数点数のフォーマット

IEEE754方式の概要

Unityをはじめ、多くのソフトウェアでの浮動小数点数の表現は、IEEE754方式に則っているようです。コンピューター内部は2進数で処理されており、浮動小数点数のメモリー領域は次のように32 bit *が確保されます。

プログラミングコラム(浮動小数点のビット列) IEEE754形式の浮動小数点数のビット列
単精度浮動小数点数の場合

32 bitのうち、符号部指数部仮数部に分かれており、次のように定義されています。

符号部(1 bit)

プログラミングコラム(符号部) 符号部

符号部は、浮動小数点数の正負を表現します。1 bitで

  • 0:正の数
  • 1:負の数

を表すことになります。

指数部(8 bit)

プログラミングコラム(指数部) 指数部

指数部は、累乗数の指数を表現します。8 bitあるので、

			0000 0000(2) ~ 1111 1111(2)
		

つまり、

			0(10) ~ 255(10)
		

の範囲が表現できます。

下付き文字のかっこ内の数値は、進法を表します。

ただし、指数部が正の数だけの場合、Fが1以上であるため、浮動小数点数で1より小さい数が表現できなくなります。1より小さい数に表現を拡張するために、8 bitで表現された値から127を減算しておくことで、

			-127(10) ~ 128(10)
		

が、表現できるようになりました(これを「バイアス」といいます)。

0111 1111(2) = 0(10)になります。

仮数部(23 bit)

プログラミングコラム(仮数部) 仮数部

仮数部は、基準となる数Fを表現します。この23桁のビット列は、仮数部Fの小数点以下の部分を定義します。

負の指数で累乗すると、正の指数の逆数になります。つまり、

			ex) 2-1 = 1/2
ex) 2-4 = 1/16

のようになります。

それぞれのビットを計算したのちに、すべてを足し合わせます。23 bitあるので、

			000 0000 0000 0000 0000 0000(2) ~ 111 1111 1111 1111 1111 1111(2)
		

つまり、

			0(10) ~ 0.9999998807907104(10)
		

の範囲が表現できます。

IEEE754方式では、この値に1を加えます(これを「正規化」といいます)。したがって、上記で述べてきたとおり、基準となる数Fは、1以上2未満の値となります。

フォーマット

ここまでをまとめると、IEEE754方式の浮動小数点数は、次のような式で表すことができます。

プログラミングコラム(IEEE754形式のフォーマット) IEEE754形式のフォーマット

浮動小数点数の限界に迫る

指数部の限界

浮動小数点数の絶対値の大小は、ほぼ指数部で決まります。ここまで説明したとおり指数部は、

			-127(10) ~ 128(10)
		

の値が取れるので、

			2-127(10) ~ 2128(10)
		

つまり

			5.88 × 10-39(10) ~ 3.40282 × 1038(10)
		

というトンデモない範囲が表現できることがわかります。

指数部だけを見たときに、実使用上では限界を意識する必要はないでしょう。

仮数部の限界

仮数部の表現範囲を確認します。1と次に大きい数値を比べると、

			1.000 0000 0000 0000 0000 0000(2) = 1(10)
1.000 0000 0000 0000 0000 0001(2) = 1(10) + 2-23(10) = 1.000000119209...(10)

となり、仮数部の最小分解能は、

			0.000000119... ≈ 1 × 10-7
		

になります。

やっとここで、冒頭で述べた7桁という数字が出てきました!

ただ、仮数部の分解能が、なぜ浮動小数点数全体の限界となるのでしょう。上で確認した分解能が影響するのは、どういう場合でしょうか。

それは絶対値の離れた数どうしの足し算です。

どんどん深淵へとはまっていく気がしますが、浮動小数点数の加算方法を確認してみましょう。

浮動小数点の足し算

足し算の流れ

限界うんぬんを確認する前に、浮動小数点数の足し算について説明します。手順としては、次のとおりです。

  1. 10進数でF × 2Eに変形
  2. ビット化対象の確定
  3. 2進数に変換
  4. 指数部どうしを一致
  5. 仮数部どうしを加算

例を併記しながらがわかりやすいと思うので、とある2つの数ABを用意しました。

  • A:12.15(10)
  • B:0.089(10)

実際にこれを足し算してみましょう。

①10進数でF × 2Eに変形

Fが1以上2未満になるように変形します。

つまり、整数部が1になるようにすれば良いわけですね。2進数で扱う都合から、2を掛けたり、2で割ったりして調整します。

プログラミングコラム(フォーマットに合うように変形します。) 尺度一致のための変形

と、整数部が1になるまで乗除算を繰り返します(最初から整数部が1の場合は、そのまま)。

乗除算をして、もとの数を破壊してしまったので、辻褄を合わせます。逆数となる2の累乗数を掛け算します。

				A:1.51875 × 23 = (12.15 × 2-3) × 23
B:1.424 × 2-4 = (0.089 × 24) × 2-4

これで、F × 2Eの形になりました。

②ビット化対象の確定

①で変形したものを、さらにIEEE754形式に合わせます。

(-1)sign × (1 + fraction) × 2exponent - 127

だったので、

				A:1.51875 × 23 = (-1)0 × (1 + 0.51875) × 2130 - 127
B:1.424 × 2-4 = (-1)0 × (1 + 0.424) × 2123 - 127

となります。表現が一致したところで、それぞれのビット化対象を抜粋します。ビット化するのは、上のIEEE754形式のsignexponentfractionが該当します。

ビット化対象を抜粋
sign exponent fraction
A 0 130 0.51875
B 0 123 0.424

③2進数に変換

①②では10進数で扱ってきました。ここで10 → 2進数に変換します。

exponent部(10 → 2進数の整数部変換)
  1. 10進数の数値を、2で余剰演算して余りを記録する
  2. ⅰを0になるか、桁あふれ(8桁)するまで繰り返す
  3. 記録した余り(0 or 1)を新しいものから左詰めで並べる

例(A)では、

プログラミングコラム(指数部を2進数に置き換えます。) 指数部の2進数化
fraction部(10 → 2進数の小数部変換)
  1. 10進数の数値を、2で乗算して整数部を記録する
  2. 乗算結果の小数部を抜き取る
  3. ⅰ~ⅱを小数部が0になるか、桁あふれ(23桁)するまで繰り返す
  4. 記録した整数部(0 or 1)を小数点以下に左詰めで並べる

例(A)では、

プログラミングコラム(仮数部を2進数に置き換えます。) 仮数部の2進数化

それぞれの計算結果は、次のようになりました。

2進数への変換結果
sign exponent fraction
A 0 1000 0010 .100 0010 0110 0110 0110 0110
B 0 0111 1011 .011 0110 0100 0101 1010 0001

④指数部どうしを一致

2.5 kgと10 gを足し算するには、尺度を一致させる必要がありますよね。

				ex) 2.5 kg + 10 g
   = 2.5 × 103 g + 0.01 × 103 g
   = (2.5 + 0.01) × 103 g = 2.51 × 103 g

10進数で考えるとわかりやすいですね。

これと同じことを2進数でやっていきます。2つの数の指数部を一致させて、仮数部を足し合わせます。

浮動小数点数では、絶対値が大きいほうの尺度に合わせるというルールがあります。
指数部の差
指数部 exponent(10) exponent(2)
A 3 130 1000 0010
B -4 123 0111 1011

指数部の差を1詰めるのは、つまり全体を2倍することと同義です。

				ex) 1.0 × 23 = 8
ex) 1.0 × 24 = 16

AとBにおいては差が7なので、2 × 2 × 2 × 2 × 2 × 2 × 2 = 27倍することになります。

ただ27倍するだけでは、もとの数を破壊してしまうので、辻褄を合わせます。指数部をずらした分だけ仮数部を調整します。指数部を1ずらすと2倍になったので、仮数部では逆に1/2にしていきます。

仮数部は、ビットを右シフトしていくと1/2になります。

				ex) 0.0110(2) = 0 × 2-1 + 1 × 2-2 + 1 × 2-3 + 0 × 2-4 = 0 + 1/4 + 1/8 + 0 = 3/8
     ↓ ビット右シフトで1/2になっている。
ex) 0.0011(2) = 0 × 2-1 + 0 × 2-2 + 1 × 2-3 + 1 × 2-4 = 0 + 0 + 1/8 + 1/16 = 3/16

Bを検算しながら、一桁ずつ右シフトしていきます。

このとき、シフト前の整数部に1を含めることに注意しましょう。

プログラミングコラム(ビットシフトの動き) ビットシフト

後ほど回収しますが、fractionの情報が右シフトで欠落していっていることに着目してください。桁があふれて消失してしまっています。とはいうものの、検算ではおおむね値を維持したまま変形できていることがわかります。

Bは変形を経て、

Bの変形結果
exponent fraction
B 1000 0010 .000 0001 0110 1100 1000 1011

となりました。ここで注意しておきたいのは、この値をIEEE754形式のフォーマットに入れて確かめようとしても、意味がないということです。上記変形では、fractionだけではなく、整数部1を含む仮数部まるごとシフト計算してしまっているためです。フォーマットでは、さらに整数部に1を加えるため、正しい値が得られません。

逆に、fractionに1を加えなければ、正しい値が得られます(検算でやっているのはこれです)。

⑤仮数部どうしを加算

指数部、仮数部ともに尺度がそろったので、仮数部を足し合わせます。

				A:  1.100 0010 0110 0110 0110 0110
B:  0.000 0001 0101 1100 1000 1011

A+B:1.100 0011 1101 0010 1111 0001

最終的には、次のようになりました。

A+Bの結果
sign exponent fraction
A + B(2) 0 1000 0010 .100 0011 1101 0010 1111 0001
A + B(10) 0 130 .529874921

最後にフォーマットを代入して、計算値があっているかを確認しましょう。

				(-1)0 × (1 + 0.529874921) × 2130-127
= 1 × 1.529874921 × 23
= 1.529874921 × 8
= 12.238999368
≈ 12.239

A、Bを10進数で加算すると、

				A:12.15
B:0.089
A+B = 12.239

で、ほぼ一致しています。

お疲れ様でした!!浮動小数点数の足し算をマスターしました!

浮動小数点数の足し算は情報が消失する

ということで、だいぶ遠回りをしてきましたが、得られてコトが2つあります。

そうです。浮動小数点どうしを足し算する場合、

尺度が違いすぎると小さい方はなかったことになる 情報消失は10進数で7桁程度の差が開いたとき

ということになります。

例で桁があふれるのは体感してもらいましたが、結果として誤差範囲でした。しかし、これが2進数で23桁以上離れていると、足し算しても・しなくても値が変わらない状態になります。

例えば、

  • C:1 × 224(2)
  • D:1 × 20(2)

のような数を足し算してみます。

CとDの要素
指数部 仮数部
24 1.000 0000 0000 0000 0000 0000
0 1.000 0000 0000 0000 0000 0000

となります。指数部の差は24なので、足し算するには値が小さいDをシフトします。

プログラミングコラム(情報が消失しています。) ビットシフトによる情報の消失

シフトを追って見てみると、24回目のシフトで持っていた値が完全に消失しました。つまり、

2進数で24桁以上差が開いた2つの数値は正しく加算できない

ということになります。

ところで、図中に見たことのある数があります。

		0.00000011920928955078125
	

2進数で23桁を、10進数表現するとおよそ7桁になるということです。

一応、計算もしておきます。2進数でy桁、10進数でz桁の比率は、

		2y = 10z
log102y = log1010z
y × log102 = z × log1010
y × 0.30103... = z × 1
z / y = 0.30103...

となり、約0.3倍の比例関係になります。つまり、

		23 桁 * 0.30103... = 6.927...
	

となり、「2進数で23桁」=「10進数で7桁程度」といえます。

足し算をすると10進数で7桁程度で桁あふれが発生し、情報が欠落する事実にたどり着きました。

実際には、ビットによって精度が狂い始めるタイミングはズレがあります。

参考サイト

記事執筆にあたり、次のサイトを参考にさせていただきました。

まとめ

今回は、「float」すなわち「浮動小数点」の限界を見てきました。結論としては、足し算の際のビットシフトによる桁あふれによって、

浮動小数点は、10進法で7桁程度が精度の限界

ということが理解できました。

初めてコラムというカタチを選んでみましたが、普段書いている記事の数倍の長さになってしまいました。ここまでお付き合いいただきありがとうございました。

コメント