【TECH BLOG #56】丸め誤差への対策

グリーエンターテインメント株式会社 エンジニア部の白井です。
今回は丸め誤差についてです。
担当しているタイトルであった話なのですが、とあるパラメータが稀に想定値とズレるという問題が起きました。とても稀なのですが特定の組合せの時に「1」だけ誤差がでる。ピンと来て、調べてみたところ原因は丸め誤差でした。
丸め誤差が発生する原理
小数を限られた桁数内で表現するためには、切り捨てなどの修正が必要です。この際、本来の値との間に発生するのが丸め誤差です。コンピュータ上で小数を扱う際は、2進数/10進数の変換時に循環小数となって発生する事が多いと思います。
簡単な例としては、このような処理で再現できます。
----------------Python 3.11.8 a = 0.1 b = 0.2 c = a + b if c == 0.3: print("OK") else: print("NG") ---------------- > NG ----------------
結果は「NG」となります。
変数「c」の結果が「0.30000000000000004」となって一致しません。二進数の浮動小数点数で表現した際、循環小数となってしまうのが原因です。この場合では「0.1」の仮数部分が「1001100110011…」のような形になっています。
解決方法の紹介
よく使われる解決方法をいくつか紹介したいと思います。
解決方法その1:10進数表現のモジュール等で解決
Pythonではこんな感じになります。
----------------Python 3.11.8 from decimal import Decimal a = Decimal('0.1') b = Decimal('0.2') c = a + b if c == Decimal('0.3'): print("OK") else: print("NG") ---------------- > OK ----------------
C#では、floatやdoubleの代わりにdecimal型を使用することで解決できます。C++ではBCDなどを使用する形になると思います。
例えばBCDだと十進数の1桁ずつを各4ビットで表現する形になります。簡単で確実なんですが、計算時の処理が増えるのでパフォーマンスは良くないです。なので、ゲーム開発では別の手段で解決することも多いと思います。
解決方法その2:イプシロン定数での解決
イプシロンはギリギリ0ではない凄く小さな値です。例えば切り上げ処理での誤差を防ぐ為に、イプシロンを足してから切り上げる等の使い方をします。
C#では「Double.Epsilon」等のように各小数の型毎に定義されています。ただしC#のイプシロン定数は、厳密には計算機イプシロンではないので注意が必要です。「計算機イプシロン」の定義は、そのコンピューターで扱える「1より大きい最小の数」です。「C#のイプシロン定数」は演算時の誤差上限値で、丸め誤差が起きた際にズレる恐れのある最大値を示しています。
解決方法その3:固定小数点数での解決
有効桁数分を掛け、計算した後に割る事で、整数変数を使って小数を表現します。
----------------Python 3.11.8 bias = 100.0 a = int(0.1 * bias) b = int(0.2 * bias) c = (a + b) / bias if c == 0.3: print("OK") else: print("NG") ---------------- > OK ----------------
掛け算・割り算をシフト演算に置き換えて2進数で行う場合もあります。10進数上での有効桁数が分かり辛くはなりますが、切り捨てをAND演算で行えたり、処理の効率が良いです。昔のゲーム開発では浮動小数点数の処理が重かったりしたため、これが良く使われていました。SIMDでの最適化にも役立つ方式です。
まとめ
いかがでしょうか。小数を扱う限り必ず付いて回りますし、発生時に原因が分かり辛い問題です。ただ、予め「誤差が起きるもの」という認識さえしておけば、取れる対策の多い問題でもあると思います。
紹介した以外にも色々と出来る対策はあり、行列計算などのライブラリには予め対策されている物もあったりします。怖がらずに頑張って対応していきましょう。
本件に関するお問い合わせ先
グリーエンターテインメント株式会社 広報担当
東京都港区六本木6-11-1 六本木ヒルズゲートタワー
E-mail:info-ent@ml.gree.net