Edited at

浮動小数点数の限界を把握する

いまやあらゆるプログラムで浮動小数点数が使われていますが、ここで改めてその性質を確認してみましょう。


フォーマットのおさらい

浮動小数点数には単精度(float)と倍精度(double)がありますが、例えばゲームプログラムであれば使用頻度が高いのはfloatでしょう。

floatのフォーマットはIEEE754という規格で

符号部:1bit

指数部:8bit

仮数部:23bit

と定められています。

指数部には負の値が欲しいので(数値の符号とは別に)、あらかじめ127を加算した値を指数部に格納します。(追記:コメントに議論あり、規格策定の歴史的経緯に言及しているわけではありません)

よって指数部にゼロ乗を格納したい場合は上の図のように 01111111 が入ります(という規格です)。

仮数部は1.0を基準に考えたいので、あらかじめ1.0を減算した値を格納します。

よって仮数部に1.0を格納したい場合は上の図のように 00000000000000000000000 が入ります(という規格です)。

結果、たとえば 1.0 という値を表現する浮動小数点数は二進数で 0 01111111 00000000000000000000000 となります。

4つずつ区切ると

0011 1111 1000 0000 0000 0000 0000 0000

なので、16進数だと

3f800000

となります。ベテランプログラマは全員、 1.0 が 3f800000 であることをこっそり暗記しています(たぶん)。


限界を調べる

指数部は「2の何乗か」を示します。よって、指数部に1を加えるだけで二倍の値が表現できます。IEEE754によれば指数部には-126から127まで格納できるので、ものすごく小さな値からものすごく大きな値が表現できるわけです。(指数に慣れていない方は指数の負の値についておさらいしましょう。たとえば指数に-2を入れると「2の2乗分の1」つまり $\frac{1}{4}$ になり、指数に-126を入れたときは「2の126乗分の1」という、とんでもなく小さな値になります)

大きな数値も小さな数値も表現できてスバラシイ。でもここで問題になるのは、大きな数値と小さな数値が同居する場合です。例えば10進数で

10000000.00000001

のような数値ですね。有効数字は仮数部23bitで定められているので、そこに限界があるでしょうよ、という話です。


23bitの限界

仮数部は23bitあるので、指数部がゼロ(=01111111)だった場合$\frac{1}{2}^{23}$まで有効数字があります。計算してみると

$\frac{1}{2}^{23}=0.00000011920928955078125$

こんな値です。よって指数部がゼロ乗だとすれば「1.0に加算できる粒度は0.00000011920928955078125が限界」、つまり

1.00000011920928955078125

という値が、表現できる限界ということになります。

2進数ベースの理屈をここでは10進数で書いているので端数が長いんですが、何進数であっても有効数字の理屈は変わりません。

例えば全体を1000倍したら「1000に加算できるのは0.00011920929」が限界、つまり

1000.00011920928955078125

という値が限界です。1000倍が2のべき乗でないため、厳密には少し値がズレますけど。ざっくり言えば、

「1000という値には0.0001という値を加えるのがギリギリ」

と考えられるわけです。


問題が起きるパターン


距離で起きる問題

そんな限界から、ヤバいパターンを考えてみましょう。

1.0が1メートルだった場合。1000メートルは1キロメートルで、0.0001は 0.1ミリメートルです。先ほどの限界の話をここに適用すると、

「原点から1キロ離れた位置では0.1ミリを加算するのがギリギリ」

という話になります。

さらに10倍してみると、

「原点から10キロ離れた位置ではもう1ミリがギリギリ」

という話にもなります。これはけっこう無視できない値です。例えばゲームにおいて、

「西に1時間ほど歩き続けたらなんかぶっ壊れた」

なんてことは当たり前に発生しうるので、必ず意識しておく必要があります。


時間で起きる問題

距離の話で登場した数値を時間に換算すると、

「1000秒に0.1ミリ秒加算するのがギリギリ」

なわけです。では24時間経過した世界を考えると、

24H=24x60x60=86400秒

で、ここは改めて真面目にfloatの限界値を計算してみると

0.0078125

が限界です。つまり

86400.0078125

が限界なので、

「24時間経過すると、分解能は7ミリ秒程度しかない」

という話になります。60fpsのゲームにおいては、24時間後にはもうdeltaTの加算が危うい、ということです。24時間を秒に換算した 86400.0 に、1/60秒を足そうとして 0.016666 を加算しても、実際には 0.015625 しか足されません。


どう対処するか

この問題への対処ですが、ひとことで言えば「大きな値に小さな値を加算する時には気をつけようね」という話になってしまいます。

要するに具体的に言及しても責任取れませんすみません、ということですが、あえて対処法の第一候補としてオススメしたいのは、倍精度浮動小数点数(double)を使用することです。

doubleの仮数部は52bitあります。これだけあれば相当な無茶も許容されます。試しに24時間後を計算してみると、限界は

86400.000000000014551915228366851806640625

のようです。(floatの限界は 86400.0078125 でしたね!)

つまり24時間経過してもなお、約0.01ナノ秒の表現力を、doubleなら持っているわけです。例えば100日後、つまり3ヶ月動かしっぱなしでも約1ナノ秒の分解能を維持しているという、この安心感。


doubleは遅いのか

doubleについて、floatと比較してパフォーマンスが気になるかもしれません。メモリアクセスにも関連するので一概には言えませんが、現代においてはdoubleの演算はほとんど問題にならない速度で実行できます。プロセッサによってはdoubleのほうが速いことすらあります。ゲームロジックならぜんぶdoubleで良いと言ってもいいぐらいです。ただし現代のGPU(つまりシェーダ)はdoubleを扱えないので、なにもかもdoubleにするわけにはいきません。


まとめ

floatは精度がね、などとよく言われますが、具体的にどのぐらいに限界があるのか、という話でした。

特に、加算しっぱなしの時間、なんてものがプログラム中にあったら、floatは相当ヤバイです。doubleを検討しましょう。

いっぽうで累積の要素がないのであれば、floatで問題が起きることはあまりないでしょう。(なのでGPUでdoubleを扱えるようになる日は来ないのではないか、という気がします)

また物理を扱う場合は微分値を加算するケースが多いので、精度が落ちる地点はもっと近くにくるはずです。

それから、floatもdoubleも1.0が起点になっているため、1.0近辺の精度がもっとも高いことを覚えておくと良いです。

これはちょっとしたコツですが、ゲームを作るときは、自キャラを1.0ぐらいにしておく(戦艦のゲームなら全体のスケールを1/100にする)と、のちのトラブルに巻き込まれにくいと思います。


(追記:不正確な記述なので削除しました)