Javaの浮動小数点数に関するいくつかの言葉



数日前、私はこのコードを実行するとどのような結果になるかなど、面白い質問に出会いました。

double a = 2.0 - 1.1;
      
      





またはそのような:

 double f = 0.0; for (int i=1; i <= 10; i++) { f += 0.1; }
      
      





私の期待に反して、答えは最初のケースでは0.89999999999999991で、2番目のケースでは0.9999999999999998989です。

なぜ、このタイプのデータに関するいくつかのより興味深い事実を知りたい人のために、歓迎します。







一般的に、上記の質問に対する答えは次のように聞こえます。「同様のエラーは、数値の内部バイナリ表現に関連しています。 10進法では1/3の除算の結果を正確に表すことはできないので、2進法では1/10を正確に表すことはできません。 丸めエラーを排除する必要がある場合は、BigDecimalクラスを使用する必要があります。



πや0.2などの抽象実数とJavaのdoubleデータ型には重要な違いがあります。 まず、実数のプラトニックな理想的な表現は無限ですが、Javaの表現はビット数によって制限されます。 ただし、計算精度は、数値のサイズを制限するよりも緊急の問題です。 さらに「興味をそそる」ことは、数字を四捨五入する完全に独創的な方法ですが、順番にすべてについて。



おそらく、整数のバイナリ表現から始める必要があります。 この段落は後で役立ちます。 だから。 整数を表現するための最も簡単なオプションは、いわゆる「直接コード」であり、最上位ビットは数値の符号を記録するために使用され(0-正、1-負)、残りのビットは値自体を記録するために直接使用されます。 したがって、8ビット表現の数字「-9」は10001001のようになります。このアプローチの欠点は、2つのゼロ(「+0」および「-0」)の存在と負数による算術演算の複雑さです。 興味のあるもう1つのオプションは「シフト付きコード」です。簡単に言えば、このタイプの表現に2 ^(n-1)に等しい特定の定数を追加します。nはビット数(ビット)です。 。 私たちの場合、8ビット表現で数字「-9」を使用した例は次のようになります。

-9 + 2 ^(8-1)= -9 + 128 =119。バイナリ形式では、01110111が得られます。このオプションはゼロが1つしかないため便利ですが、算術演算ではオフセットを考慮する必要があります。



ここでこれに言及する価値があります。 Java言語の目標の1つは、マシンの独立性です。 どの仮想マシンが計算を実行しても、計算は同じ結果を生成するはずです。 浮動小数点数の算術計算の場合、これは予想外に難しい作業であることが判明しました。 double型は64ビットを使用して数値を格納しますが、一部のプロセッサーは80ビット浮動小数点レジスターを使用します。 これらのレジスタは、計算の中間段階で追加の精度を提供します。 中間計算結果は80ビットのレジスタに保存され、その後、応答は64ビットに丸められます。 ただし、64ビットプロセッサがすべての計算で使用される場合、この結果は異なる場合があります。 このため、JVMの元の説明では、すべての中間計算を丸める必要があると述べていました。 このような丸めはオーバーフローにつながるだけでなく、計算自体が遅いため、多くの専門家からの抗議が発生しました。 これは、JDK 1.2がstrictfpキーワードのサポートを導入し、このメソッド、クラス、またはインターフェース(またはその実装)内で実行されるすべての計算の結果の再現性を保証するという事実につながりました。 言い換えると、strictfpキーワードは、プラットフォームによってはより高い精度で計算を実行できる場合でも、各プラットフォームで浮動小数点計算が一定の精度で同じように動作することを保証します。 興味深いことに、x86ファミリのプロセッサでは、浮動小数点演算モジュールが数学浮動小数点ユニット(FPU)と呼ばれる別のチップに割り当てられています。 Pentium MMXプロセッサ以降、浮動小数点モジュールは中央プロセッサに統合されています。 詳細



次。 IEEE 754標準では、実数の表現は指数関数的に記述する必要があるとされています。 これは、ビットの一部が番号のいわゆる仮数をエンコードし、他の部分が順序(度)のインジケータであり、別のビットが番号の符号を示すために使用されることを意味します(0-番号が正の場合、1-番号が負の場合)。 数学的には、次のように書かれています。

(-1)^ s×M×2 ^ E 、ここでsは符号、Mは仮数、Eは指数です。 出展者は上記の式で取得できるシフトで記録されます。



仮数と指数とは何ですか? 仮数は、実数の最上位ビットを表す固定長の整数です。 仮数が4ビット(| M | = 4)で構成されているとします。 たとえば、バイナリシステムでは1001になる番号「9」を取り上げます。

指数(「順序」または「指数」とも呼ばれます)は、最上位の底(2)の次数です。 これは、数値の小数部分を区切るポイントの前の桁数と見なすことができます。 指数がレジスタに書き込まれ、コンパイル時に不明な変数である場合、その数値は「浮動小数点数」と呼ばれます。 指数が事前にわかっている場合、その数値は「固定小数点数」と呼ばれます。 固定小数点数は、仮数のみを保存することにより、通常の整数変数(レジスタ)に書き込むことができます。 浮動小数点数を記録する場合、カマキリと指数の両方が、いわゆる「1.001e + 3」などの標準形式で記録されます。 仮数が4つの符号で構成され、指数が3であることはすぐにわかります。



仮数の同じ3ビットを使用して小数を取得するとします。 たとえば、E = 1の場合、これを行うことができます。 その後、私たちの数は等しくなります



1.001e + 1 = 1×2 ^ 2 + 0×2 ^ 1 + 0×2 ^ 0 + 1×2 ^(-1)= 4 + 0.5 = 4.5







このアプローチの問題の1つは、同じ仮数の長さ内の同じ数字の異なる表現です。 仮数の長さが5である「9-ku」は、1.00100e + 3および0.10010e + 4および0.01001e + 5として表すことができます。 これは、機器にとっては便利ではありません。 数値を比較するとき、およびそれらに対して算術演算を実行するときは、表現の多様性を考慮する必要があります。 さらに、これは、表現の数が有限であり、繰り返しはまったく表現できる数の数を減らすため、経済的ではありません。 ただし、ちょっとしたトリックがあります。 最初のビットの値を計算するには、指数を使用できます。 指数のすべてのビットが0の場合、仮数の最初のビットもゼロに等しいと見なされ、そうでない場合は1に等しくなります。 仮数の最初のビットが1に等しい浮動小数点数は正規化されます。 ゼロに等しい仮数の最初のビットである浮動小数点数は、非正規化と呼ばれます。 彼らの助けを借りて、はるかに少ない量を表すことができます。 最初のビットは常に計算できるため、明示的に保存する必要はありません。 暗黙的なユニットをメモリに保存する必要がないため、これにより1ビットが節約され、数値の一意の表現が提供されます。 「9」の例では、正規化表現は1.00100e + 3になり、仮数は「00100」としてメモリに保存されます。 上位ユニットは暗黙的に暗示されます。 このアプローチの問題は、ゼロを表すことができないことです。これについては後で説明します。 これについての詳細はこちらこちらをご覧ください



ところで、JDK 1.5では、浮動小数点数を16進形式で指定できます。 たとえば、0.125は0x1.0p-3として表すことができます。 16進表記では、指数を示すために「e」の代わりに文字「p」が使用されます。



Doubleを使用する場合の注意事項:



  1. 0による整数除算は例外をスローしますが、浮動小数点数の0による除算は無限大になります(0.0 / 0の除算の場合はNaN)。 ところで、JVM開発者は、同じIEEE 754標準に従って、それぞれ-1.0 / 0.0と1.0 / 0.0に等しいDouble.NEGATIVE_INFINITYとDouble.POSITIVE_INFINITYの値も導入したことを知りたいと思いました。
  2. Double.MIN_VALUEは、実際にはdoubleで記述できる最小の数ではありません。 IEEE 754標準に従って、仮数の最高単位が暗黙的に指定されているという事実について話したことを覚えていますか? だからここに。 前述のように、正規化された形式の浮動小数点数でゼロを表すことは不可能です。ゼロに等しい2のべき乗がないためです。 そして、この問題を解決するためのJVM開発者は、Double.MIN_VALUEという変数を導入しました。これは実際、ゼロに最も近い値です。 doubleに保存できる最小値は「-Double.MAX_VALUE」です。

     System.out.println(0.0 > Double.MIN_VALUE); //  false
          
          



  3. 前のトピックを作成して、別の興味深い例を挙げて、一見しただけではすべてが明白ではないことを示します。 Double.MAX_VALUEは1.7976931348623157E308を返しますが、浮動小数点数を含む文字列をdoubleに変換するとどうなりますか?



     System.out.println(Double.parseDouble("1.7976931348623157E308")); // (...7E308) = 1.7976931348623157E308 max value System.out.println(Double.parseDouble("1.7976931348623158E308")); // (...8E308) = 1.7976931348623157E308 same??? System.out.println(Double.parseDouble("1.7976931348623159E308")); // (...9E308) = Infinity
          
          







    Double.MAX_VALUEとDouble.POSITIVE_INFINITYの間には、計算時に一方または他方に丸められる値がいくつかあることがわかります。 ここでは、さらに詳しく説明する価値があります。



    実数のセットは無限に密です。 次の実数のようなものはありません。 任意の2つの実数については、それらの間隔に実数があります。 このプロパティは、浮動小数点数には当てはまりません。 タイプfloatまたはdoubleの各番号には、次の番号が存在します。 さらに、float型またはdouble型の2つの連続した数値の間には最小の有限距離があります。 Math.nextUp()メソッドは、指定されたパラメーターを超える次の浮動小数点数を返します。 たとえば、このコードは1.0から2.0までのすべての浮動小数点数を出力します。



     float x = 1.0F; int numFloats = 0; while (x <= 2.0) { numFloats++; System.out.println(x); x = Math.nextUp(x); } System.out.println(numFloats);
          
          







    1.0から2.0までの範囲には、8,388,609個の浮動小数点型の数値があります。 これはたくさんありますが、同じ範囲内にある無限の実数よりはるかに少ないです。 連続する浮動小数点数の各ペアは、約0.0000001離れています。 この距離は、最小精度単位(ULP)と呼ばれます。 double型の場合、小数点以下の数字の数がはるかに多いことを除いて、状況はまったく同じです。




おそらくそれだけです。 「もう少し深く掘り下げたい」人は、次のコードを使用できます。



 //           IEEE 754 long lbits = Double.doubleToLongBits(-0.06); long lsign = lbits >>> 63; //  long lexp = (lbits >>> 52 & ((1 << 11) - 1)) - ((1 << 10) - 1); //  long lmantissa = lbits & ((1L << 52) - 1); //  System.out.println(lsign + " " + lexp + " " + lmantissa); System.out.println(Double.longBitsToDouble((lsign << 63) | (lexp + ((1 << 10) - 1)) << 52 | lmantissa));
      
      







マスターした皆さんに感謝します。 私は建設的な批判と追加を喜んでいます。



関連資料:

新しいJava数学機能、パート2:浮動小数点数

IEEE標準754浮動小数点数

Java言語と仮想マシンの仕様

実数の表現

浮動小数点演算について知っておくべきこと

浮動小数点数の算術演算



All Articles