POSTD PRODUCED BY NIJIBOX

POSTD PRODUCED BY NIJIBOX

ニジボックスが運営する
エンジニアに向けた
キュレーションメディア

POSTD PRODUCED BY NIJIBOX

POSTD PRODUCED BY NIJIBOX

ニジボックスが運営する
エンジニアに向けた
キュレーションメディア

FeedlyRSSTwitterFacebook
edA-qa mort-ora-y

本記事は、原著者の許諾のもとに翻訳・掲載しております。

浮動小数点数はどこにでもあります。これを使わないソフトウェアは、簡単には見つかりません。ソフトウェアの記述に不可欠な何かのために、浮動小数点数を扱う際に私たちが非常に注意を払っているのだと思われるかも知れませんが、普通はそうではありません。多くのコードでは、浮動小数点数は実数として扱われ、多くのコードが無効な結果を生みます。この記事では、浮動小数点数の反直感的な性質をいくつか紹介します。 これらの性質は、計算を正確に行うために知っておかなければならないことです。

x + y == x

この第1の規則は、大きさの規則です。加算および減算をする際、お互いの数が他方の数に対して、有意味な結果を生めるだけの大きさが必要です。ここで大きさは、指数部の差を尺度とします。

例えば、値 1e-10 の大きさは、 1e10 に比べてとても小さいです。通常の64ビット浮動小数点数では、この小さな数を好きなだけ加算したり減算したりしても、 1e10 の範囲内に収まります。浮動小数点は、大きさの小さい数の違いに注目するには十分な精度がありません。

大きな値や小さな値を扱うアルゴリズムでは、この制限に注意する必要があります。

n * x != x + … + x

これは、上記の規則から得られる、精度に関する基本規則です。ある数を n 回繰り返して加算しても、 n を乗算するのと同じ結果にはなりません。 たとえ x が和に比して十分大きい場合でも、結果は異なります。必ず、丸めが伴います。2つの浮動小数点数を加算した時、その和が実数として計算した際の和と完全に一致することは、ほとんどありません。

浮動小数点数では、繰り返し計算は可能な限り避けるべきです。閉形式のアルゴリズムを見つけると、通常、より高い精度が得られます。繰り返しを避けられない場合は、期待する結果は得られないということを理解しておくことが重要です。一定量の丸め誤差が、必ず累積します。

(x ⊕ y) ⊕ z != x ⊕ (y ⊕ z)

+ - * などの、多くの2項演算子では、実数は結合的性質をもちます。式は、異なった方法でグループ化したり、異なった順序で演算したりすることができ、結果は同じです。
結合的性質は、浮動小数点数には正確には当てはまりません。これは、精度に関する上記の規則から得られます。演算の順序を変えると、結果が変わります。たとえ、すべての値の大きさが同様になるよう注意しても、やはり結果は異なります。

このことが、私たちが高精度の浮動小数点数を使用することの1つの理由です。普通、結果に完全な精度は必要ないので、小さな累積誤差は大丈夫です。私の最近のグラフィックスプロジェクトからの例を考えます。画面にオブジェクトを置きたい場合は、正確な画素位置に数値を丸めます。値 13 13.1 13.00000123 は、すべて同一として扱われ、画素 13 に置かれます。結果の大きな部分を変えることがなければ、累積誤差は問題になりません。

x != 0

除算を伴う数式では、ゼロ除算に注意する必要があります。以下のような条件分岐をするコードがよくあります。

if (x != 0)
  y = b / x
else
  y = 0

一般に、このコードは破綻しています。 x が計算結果であるなら、実際にはゼロにはならないでしょう。しかしその代わり、丸め誤差と精度に起因して、非常にゼロに近い数になる場合もあるでしょう。ゼロ除算は回避されますが、とても小さな数で除算したときには、私が見てきたほとんどの数式は等しく失敗していました。ここでも、問題は大きさです。除数が小さくなるにつれて、結果の大きさが増します。途方もない大きさの数は、数式の残り部分で不正確な結果を生み続けます。

この規則は、0だけでなく、どんな数の等価性チェックにも厳密に適用されます。 if ( abs(x) < 1e-6 ) などのような、この演算には、ある範囲の大きさが必ず必要です。

例外が1つあります。浮動小数点変数の範囲に収まる定数は、正確に保存されます。このことにより、ある値に真の 0 または 1 を割り当てることができ、さらに後からチェックすることもできます。あるコードが明示的に 0 値を割り当てる場合は、 x == 0 のチェックは成功します。

x / b = inf

上記の規則に留意しないと、結果が無限数になることがよくあります。除算の結果が大きすぎて、浮動小数点数の範囲を超える場合は、単に、特殊な「無限」数になります。
いちど変数が無限になると、そのままになる傾向があります。 x = inf なら、 x + y = inf x / y = inf x * y = inf, などのようになります。より問題が大きくなって、さらに、 x / x = nan x - x = nan のようになります。 nan は特殊な「非数」シンボルで、それ自体の規則をもちます。 nan のある演算の結果はすべて nan になるので、無限よりも終局的です。

x != y

0 のような特定の数と比較できない場合、2つの浮動小数点数の比較が難しくなるのは明白です。計算の精度の影響により、全く同じ入力をしない限り、1つのアルゴリズムから同じ数を得ることがほとんどできなくなります。また、2つのアルゴリズムの結果を比べると、同じ数を得られる可能性は更に下がります。

まずは数同士の比較を避けることを目指すべきですが、必要な場合もあるでしょう。典型的な対処法として、 if( abs(x - y) < 1e-6 ) のように数を減算し、イプシロンに対して十分な近似となるか比較する方法があります。

また、等価であることを直接比較できない場合は、 <= >= などの演算子も問題を引き起こします。ここでもイプシロンは有効で、 if (x >= y) ではなく if( (x - 1e-6) > y ) のように表現することができます。

x != 0.1

浮動小数点数は2進数で10進数ではないため、 0.1 などの10進数で正確に表すことができるいくつかの数は、浮動小数点数では正確な数値に変換することができません。ここに誤差が生じることになります。128ビットの浮動小数点数であっても 0.1 の正確な値を表すことはできません。分数の 1/3 を考えてみましょう。10進数型で正確に表そうとすると、 0.\overline{3} と小数部分が永遠に繰り返されることになります。同じことが2進数でも起こり、10進数の 0.1 は2進数では 0.0\overline{0011} と表されます。このような無限数列を変換できる固定ビット数は絶対に存在しません。

精度の規則に関して考えることが重要です。10進数の精度については、何度も何度も検討してきました。 0.1 は、ただ1つの10進数の数を表しているように見えますが、それは明らかな間違いです。精度の規則がこっそり忍び寄り、単純だった10進数の演算をあっという間にめちゃくちゃにしてしまいます。64ビットの浮動小数点数、 0.3 + 0.6 = 0.89999999999999991 について考えてみてください。精度に関する問題は本当に簡単に起こります!

x = expr; x != expr

拡張した浮動小数点精度に起因する問題を見つけました。x86系やx86-64系を含む、いくつかのプロセッサは80ビットの浮動小数点プロセッサを備えています。これは精度を出すにはいいのですが、注意しなければいけません。一般的に、 double 型、またはデフォルトのfloat型には64ビットしかありません。

代入演算ではビット数が80ビットから64ビットに減少するため精度が落ちますが、比較演算では expr が再度評価されるのでCPUに残る結果は80ビットになります。そして64ビットの値を読み込み、80ビットに変換して比較します。追加のビットはゼロしか持たないので値は等価になりません。

これを回避するのは非常に困難です。正確な値が決して重要にならないようにアルゴリズムを設計するのが一般的です(全ての比較には幅が伴うことを思い出してください)。しかし、これは全く問題のなさそうなコードの中に潜んでいます。いくつかの言語やコンパイラでは、変数に値を代入すると確実に精度を失ってしまいます。

float( str( x ) ) != x

理由は分からないのですが、多くの言語(ドメイン固有数学言語でさえも)において、デフォルトの浮動小数点数の文字列フォーマット化は、完全な数を保存するために十分と言える精度を持っていません。文字列フォーマットは丸められてしまうので、再度、構文解析をしても最初の数と等価にはならないでしょう。

なぜ文字列を丸めるのかを考えると理解の助けになります。2進数の数は10進数に完全にフォーマット化することができますが、絶対値の大きさが増加するにつれて桁数も増加します(2のn乗や1/2のn乗などの級数のフォーマット化を通して確認してみてください)。また、フォーマッタの精度では正確な数を得られない可能性もあるでしょう。そのため、代替手段として単純に出力を丸めているのです。

10進数を使っても、再び同じ数を得ることが可能です。切り捨てる長さが十分拡張されていれば、構文解析の結果は同じ数になるでしょう。実際の10進数の値は浮動小数点数の値と等価にならない場合もありますが、丸めることでうまく処理できます。

浮動小数点数は実数ではない

浮動小数点数は実数とは異なり、精度が固定なので、丸め誤差の影響を受け続けます。この問題は高精度の浮動小数点数では小さくできますが、完全になくなることはありません。単純な計算の場合(特に予想される10進数の結果がある場合)は、あっという間に間違った値を算出してしまいます。たとえ計算が正しくても10進数との間で起こる変換での誤差を気にしなければいけません。

全てのプロセッサで高精度浮動小数点数を処理できるわけではありません。一般的なグラフィックGPU(特に携帯電話上のもの)を考えてみると、頂点シェーダ内に32ビットの浮動小数点数しかなく、更にその内いくつかはピクセルシェーダの計算用に16ビットしかないグループに分類できます。この問題のため、私が作ったゲームの照明モデルは、使用する携帯電話によっては真っ暗なシーンになってしまいました。

他のアプリケーションより多くの精度の問題を抱えているアプリケーションがあるかもしれませんが、どちらにしても全く問題のないアプリケーションは存在しません。この問題を避けることはできないのです。プログラマとして、浮動小数点数がどのように働き、その制限はどこなのかを知らなければいけません。

私は幸運なことに、技術職からメディア関連、金融からゲームと幅広くプロジェクトに携わってくることができました。 Twitterをフォローして 、私の大好きなプログラミングの情報を共有してください。また特に興味のあることがあれば、 こちらからコメントをお送りください

監修者
監修者_古川陽介
古川陽介
株式会社リクルート プロダクト統括本部 プロダクト開発統括室 グループマネジャー 株式会社ニジボックス デベロップメント室 室長 Node.js 日本ユーザーグループ代表
複合機メーカー、ゲーム会社を経て、2016年に株式会社リクルートテクノロジーズ(現リクルート)入社。 現在はAPソリューショングループのマネジャーとしてアプリ基盤の改善や運用、各種開発支援ツールの開発、またテックリードとしてエンジニアチームの支援や育成までを担う。 2019年より株式会社ニジボックスを兼務し、室長としてエンジニア育成基盤の設計、技術指南も遂行。 Node.js 日本ユーザーグループの代表を務め、Node学園祭などを主宰。