こんにちわ、横浜すみっこクリエイター
です。ゲーム制作をする中で浮動小数点数について、悩み倒す機会がありました。そこで今回は、閑話休題、コラムというカタチでつづってみたいと思います。
軽い読み物程度~、と思って筆を執りましたが、コラムとしてはなかなかの超大作になってしまいました。。。
最後までお付き合いいただければ幸いです。
Contents
はじめに
Unityで広いマップを作ってみようと調べてみると、「どうも、floatの限界を考える必要があるようだ」ということに行き着きました。
いろいろな方が解説をされていますが、基礎知識のない私の頭では、なかなか理解が追い付きません。
いっそのこと、とことん細かく整理しながら自分なりに考え直してみることにしました。
結論
先に結論を書いておきます。
結論:「floatは、10進数で7桁程度までの精度しか担保できない」
たとえばUnityでは、距離の基準単位1 = 1.0fになっています。基準単位1 = 1メートルとしたとき、原点から10キロメートル離れた位置では、1ミリメートル以下の足し算がうまく処理できなくなっています。
では、どうして7桁に限界があるのか、順を追って見ていきましょう!
浮動小数点数とは
まずは、「float」つまり浮動小数点数について確認しましょう。
浮動小数点数は、基準となる数に、倍率を掛けて数字を表現しようというものです。
小難しい感じがしますが、この考え方はだいぶ日常生活に根付いています。たとえば、重さで考えると次のようになりますよね。
倍率 | 単位 | 累乗表現 |
---|---|---|
1/1000 | ミリグラム(mg) | × 10-3 |
基準(1倍) | グラム(g) | × 100 |
1,000倍 | キログラム(kg) | × 103 |
1,000,000倍 | トン(t) | × 106 |
1.8 kgは、1.8 × 103 gと表現できます。
数式で表現してみましょう。基準となる数をF、指数をEとすると、次のようになります。
± F × 10Eこれを「2進数でやってやろうぜ」というのが、プログラミングでの浮動小数点数になります。つまり、
± F × 2Eこれが2進数の浮動小数点数を表現する式になります。詳細は後述しますが、Fは1以上2未満の値を取ります。
上の式で「F = 1」として、Eの値を変えてみると「浮動」と呼ばれるゆえんが見えてきます。
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ビット *が確保されます。
単精度浮動小数点数の場合32ビットのうち、符号部・指数部・仮数部に分かれており、次のように定義されています。
符号部(1ビット)
符号部は、浮動小数点数の正負を表現します。1ビットで、
- 0:正の数
- 1:負の数
を表すことになります。
指数部(8ビット)
指数部は、累乗数の指数を表現します。8ビットあるので、
0000 0000(2) ~ 1111 1111(2)
つまり、
0(10) ~ 255(10)
の範囲が表現できます。
下付き文字のかっこ内の数値は、進法を表します。ただし、指数部が正の数だけの場合、Fが1以上であるため、浮動小数点数で1より小さい数が表現できなくなります。1より小さい数に表現を拡張するために、8ビットで表現された値から127を減算しておくことで、
-127(10) ~ 128(10)
が、表現できるようになりました(これを「バイアス」といいます)。
0111 1111(2) = 0(10)になります。仮数部(23ビット)
仮数部は、基準となる数Fを表現します。この23桁のビット列は、仮数部Fの小数点以下の部分を定義します。
負の指数で累乗すると、正の指数の逆数になります。つまり、
ex) 2-1 = 1/2
ex) 2-4 = 1/16
のようになります。
それぞれのビットを計算した後に、すべてを足し合わせます。23ビットあるので、
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方式の浮動小数点数は、次のような式で表すことができます。
浮動小数点数の限界に迫る
指数部の限界
浮動小数点数の絶対値の大小は、ほぼ指数部で決まります。ここまで解説したとおり指数部は、
-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桁という数字が出てきました!
ただ、仮数部の分解能が、なぜ浮動小数点数全体の限界となるのでしょう。上で確認した分解能が影響するのは、どういう場合でしょうか。
それは絶対値の離れた数同士の足し算です。
どんどん深淵へとはまっていく気がしますが、浮動小数点数の加算方法を確認してみましょう。
浮動小数点の足し算
足し算の流れ
限界うんぬんを確認する前に、浮動小数点数の足し算について解説します。手順としては、次のとおりです。
- 10進数でF × 2Eに変形
- ビット化対象の確定
- 2進数に変換
- 指数部同士を一致
- 仮数部同士を加算
例を併記しながらが分かりやすいと思うので、とある2つの数AとBを用意しました。
- 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形式のsign、exponent、fractionが該当します。
sign | exponent | fraction | |
---|---|---|---|
A | 0 | 130 | 0.51875 |
B | 0 | 123 | 0.424 |
③2進数に変換
①②では2進数で扱ってきました。ここで10進 → 2進数に変換します。
exponent部(10進 → 2進数の整数部変換)- 10進数の数値を、2で余剰演算して余りを記録する
- ⅰを0になるか、桁あふれ(8桁)するまで繰り返す
- 記録した余り(0 or 1)を新しいものから左詰めで並べる
例(A)では、
fraction部(10進 → 2進数の小数部変換)- 10進数の数値を、2で乗算して整数部を記録する
- 乗算結果の小数部を抜き取る
- ⅰ~ⅱを小数部が0になるか、桁あふれ(23桁)するまで繰り返す
- 記録した整数部(0 or 1)を小数点以下に左詰めで並べる
例(A)では、
それぞれの計算結果は、次のようになりました。
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は変形を経て、
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
最終的には、次のようになりました。
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 | 24 | 1.000 0000 0000 0000 0000 0000 |
D | 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桁程度で桁あふれが発生し、情報が欠落する事実にたどり着きました。
実際には、ビットによって精度が狂い始めるタイミングはズレがあります。参考サイト
記事執筆にあたり、次のサイトを参考にさせていただきました。
- 浮動小数点数の限界を把握する - Qiita
Unity Japanの安原さんがQiitaで解説されています。 - 浮動小数点の計算方法と注意点
まとめ
今回は、floatすなわち浮動小数点の限界に迫ってみました。結論としては、足し算の際のビットシフトによる桁あふれによって、
浮動小数点は、10進数で7桁程度が精度の限界ということが理解できました。
初めてコラムというカタチを選んでみましたが、普段書いている記事の数倍の長さになってしまいました。ここまでお付き合いいただきありがとうございました。
コメント
コメントを投稿