web-dev-qa-db-ja.com

PHP-浮動小数点数の精度

$a = '35';
$b = '-34.99';
echo ($a + $b);

結果は0.009999999999998

それは何ですか?私のプログラムがなぜ奇妙な結果を報告し続けるのか疑問に思いました。

なぜPHPは期待される0.01を返さないのですか?

76
dcmoody

浮動小数点演算!=実数演算だからです。不正確さによる違いの実例は、いくつかのフロートab、_(a+b)-b != a_です。これは、フロートを使用するすべての言語に適用されます。

浮動小数点 は有限精度の2進数であるため、有限量の 表現可能な数 があり、これは 精度の問題 とこのような驚きをもたらします。もう1つの興味深い読み物があります。 すべてのコンピューター科学者が浮動小数点演算について知っておくべきこと


問題に戻ると、基本的に、34.99または0.01を2進数(10進数のように、1/3 = 0.3333 ...)で正確に表す方法はないため、代わりに近似値が使用されます。問題を回避するには、次のことができます。

  1. 結果に round($result, 2) を使用して、小数点以下2桁に丸めます。

  2. 整数を使用します。それが通貨の場合、たとえば米ドルの場合、35.00ドルを3500、34.99ドルを3499として保存し、結果を100で割ります。

PHPが otherlanguages doのような10進数データ型を持っていないのは残念です。

118

すべての数値と同様に、浮動小数点数は0と1の文字列としてメモリに保存する必要があります。それはコンピューターのすべてです。浮動小数点と整数の違いは、0と1を調べたいときに、それらをどのように解釈するかです。

1ビットは「符号」(0 =正、1 =負)、8ビットは指数(-128〜+127の範囲)、23ビットは「仮数」(分数)として知られる数です。したがって、(S1)(P8)(M23)のバイナリ表現の値は(-1 ^ S)M * 2 ^ Pです

「仮数」は特別な形式を取ります。通常の科学表記法では、分数とともに「自分の場所」を表示します。例えば:

4.39 x 10 ^ 2 = 439

バイナリでは、「自分の場所」は1ビットです。科学表記法の左端の0をすべて無視するため(重要でない数字は無視します)、最初のビットは1であることが保証されます。

1.101 x 2 ^ 3 = 1101 = 13

最初のビットが1になることが保証されているため、スペースを節約するために数値を格納するときにこのビットを削除します。したがって、上記の数値は101(仮数用)として格納されます。先頭の1が想定されます

例として、バイナリ文字列を取り上げましょう

00000010010110000000000000000000

それをコンポーネントに分割します:

Sign    Power           Mantissa
 0     00000100   10110000000000000000000
 +        +4             1.1011
 +        +4       1 + .5 + .125 + .0625
 +        +4             1.6875

簡単な式を適用します。

(-1^S)M*2^P
(-1^0)(1.6875)*2^(+4)
(1)(1.6875)*(16)
27

つまり、00000010010110000000000000000000は浮動小数点で27です(IEEE-754規格に準拠)。

ただし、多くの数値では正確なバイナリ表現はありません。 1/3 = 0.333 ....が永遠に繰り返されるのと同じように、1/100は0.00000010100011110101110000 .....であり、「10100011110101110000」が繰り返されます。ただし、32ビットコンピューターでは、浮動小数点で数値全体を保存することはできません。だから、それは最高の推測をします。

0.0000001010001111010111000010100011110101110000

Sign    Power           Mantissa
 +        -7     1.01000111101011100001010
 0    -00000111   01000111101011100001010
 0     11111001   01000111101011100001010
01111100101000111101011100001010

(負の7は2の補数を使用して生成されることに注意してください)

01111100101000111101011100001010が0.01のように見えないことはすぐに明らかになるはずです。

ただし、さらに重要なこととして、これには、繰り返し小数の切り捨てられたバージョンが含まれます。元の10進数には、繰り返しの「10100011110101110000」が含まれていました。これを01000111101011100001010に簡略化しました

この浮動小数点数を式で10進数に変換すると、0.0099999979になります(これは32ビットコンピューター用です。64ビットコンピューターの方がはるかに正確です)

10進数の同等

問題をよりよく理解するのに役立つ場合は、小数の繰り返しを扱うときに小数の科学表記法を見てみましょう。

数字を保存する10個の「ボックス」があると仮定しましょう。したがって、1/16のような数値を格納する場合は、次のように記述します。

+---+---+---+---+---+---+---+---+---+---+
| + | 6 | . | 2 | 5 | 0 | 0 | e | - | 2 |
+---+---+---+---+---+---+---+---+---+---+

これは明らかに6.25 e -2であり、e*10^(の省略形です。必要なのは2つ(ゼロで埋める)だけでしたが、小数に4つのボックスを割り当て、符号に2つのボックスを割り当てました(数値の符号、指数の符号の1つ)

このような10個のボックスを使用して、-9.9999 e -9から+9.9999 e +9の範囲の数字を表示できます

これは、小数点以下4桁以下であれば問題なく機能しますが、2/3などの数値を保存しようとするとどうなりますか?

+---+---+---+---+---+---+---+---+---+---+
| + | 6 | . | 6 | 6 | 6 | 7 | e | - | 1 |
+---+---+---+---+---+---+---+---+---+---+

この新しい番号0.66667は、2/3と正確には等しくありません。実際、0.000003333...でオフになっています。基数3で0.66667を書き込もうとすると、0.2000000000012...の代わりに 0.2 が得られます。

この問題は、1/7のように、繰り返しの小数部が大きいものを使用した場合に、より明らかになる可能性があります。これには6つの繰り返し数字があります:0.142857142857...

これを10進数のコンピューターに保存すると、これらの数字のうち5桁しか表示できません。

+---+---+---+---+---+---+---+---+---+---+
| + | 1 | . | 4 | 2 | 8 | 6 | e | - | 1 |
+---+---+---+---+---+---+---+---+---+---+

この番号0.14286は、.000002857...によってオフになっています

「修正に近い」が、正確に正しいではないので、書き込もうとした場合基数7のこの数は、0.1の代わりに恐ろしい数を取得します。実際、これをWolfram Alphaにプラグインすると、次のようになります。 .10000022320335...

これらのわずかなわずかな違いは、0.00999999790.01とは対照的に)に馴染みがあるはずです。

47
stevendesu

ここに、浮動小数点数がそのように機能する理由について多くの答えがあります...

しかし、任意の精度についてはほとんど語られていません(Pickleが言及しました)。正確な精度が必要な場合(または必要な場合)、それを行う唯一の方法は(少なくとも有理数の場合) BC Math 拡張機能(実際には BigNum、Arbitrary精度 実装...

2つの数字を追加するには:

_$number = '12345678901234.1234567890';
$number2 = '1';
echo bcadd($number, $number2);
_

結果は_12345678901235.1234567890_...になります.

これは、任意精度の数学と呼ばれます。基本的に、すべての数字はすべての操作に対して解析される文字列であり、操作は数字ごとに実行されます(長い除算を考えますが、ライブラリによって実行されます)。したがって、それは非常に遅いことを意味します(通常の数学構造と比較して)。しかし、それは非常に強力です。正確な文字列表現を持つ任意の数値を乗算、加算、減算、除算、モジュロ演算、指数化できます。

したがって、_1/3_を100%の精度で実行することはできません。これは、小数が繰り返されるため(したがって、合理的ではありません)。

しかし、_1500.0015_の2乗が何かを知りたい場合:

32ビットの浮動小数点数(倍精度)を使用すると、次の推定結果が得られます。

_2250004.5000023
_

しかし、bcmathは次の正確な答えを提供します。

_2250004.50000225
_

それはすべて、必要な精度に依存します。

また、ここで注意すべきことがあります。 PHPは32ビットまたは64ビットの整数のみを表すことができます(インストールによって異なります)。整数がネイティブint型のサイズ(32ビットで21億、9.2 x10 ^ 18、または、署名された整数の場合は92億ドル)、PHPは整数を浮動小数点数に変換します。それはすぐに問題ではありません(システムの浮動小数点数の精度より小さいすべての整数は定義により直接表現できるため浮動小数点数として)、2つを乗算しようとすると、かなりの精度が失われます。

たとえば、_$n = '40000000002'_の場合:

数値として、_$n_はfloat(40000000002)になります。これは正確に表現されているため問題ありません。しかし、二乗すると、次のようになります:float(1.60000000016E+21)

文字列として(BC mathを使用)、_$n_はまさに_'40000000002'_になります。そして、二乗すると、string(22) "1600000000160000000004"...が得られます。

したがって、大きな数値や合理的な小数点の精度が必要な場合は、bcmathを調べてください。

14
ircmaxell

PHPのround()関数を使用します。 http://php.net/manual/en/function.round.php

この答えは問題を解決しますが、理由を説明しません。私はそれが明らかだと思った[私もC++でプログラミングしているので、IS私にとっては明らか;;]]、そうでない場合は、PHP独自の計算精度を持ち、その特定の状況では、その計算に関する最も適合した情報を返しました。

2

bcadd() はここで役立つかもしれません。

<?PHP

$a = '35';
$b = '-34.99';

echo $a + $b;
echo '<br />';
echo bcadd($a,$b,2);

?>

(明確にするための非効率的な出力)

最初の行は0.009999999999998を与えます。 2番目は0.01を与えます

2
Pickle

0.01は一連の2進小数の合計として正確に表現できないためです。そして、フロートがメモリに保存される方法です。

私はそれがあなたが聞きたいものではないが、それは質問への答えだと思います。修正方法については、他の回答を参照してください。

1
Andrey

私のPHPは0.01を返します... alt text

多分それはPHPバージョンでやる必要があります(私は5.2を使用しています)

[解決済み]

enter image description here

すべての数値は、0、1などのバイナリ値によってコンピューターに保存されます。単精度の数値では、32ビットを占有します。

浮動小数点数は、符号用に1ビット、指数用に8ビット、仮数(分数)と呼ばれる23ビットで表すことができます。

以下の例を見てください:

0.15625 = 0.00101 = 1.01 * 2 ^(-3)

enter image description here

  • 符号:0は正の数、1は負の数、この場合は0です。

  • 指数:01111100 = 127-3 = 124.

    注:バイアス= 127なので、バイアス指数= -3 +「バイアス」。単精度では、バイアスは127であるため、この例では、バイアス指数は124です。

  • 小数部では、1.01平均:0 * 2 ^ -1 + 1 * 2 ^ -2

    数値1(1.01の最初の位置)は、このように浮動小数点数が存在する場合、最初の数値は常に1であるため、保存する必要はありません。たとえば、変換:0.11 => 1.1 * 2 ^(-1)、0.01 => 1 * 2 ^(-2)。

別の例では、常に最初のゼロが削除されます。0.1は1 * 2 ^(-1)になります。したがって、最初の値は1です。現在の1 * 2 ^(-1)の数は次のようになります。

  • 0:正の数
  • 127-1 = 126 = 01111110
  • 分数:00000000000000000000000(23の数値)

最後に:生のバイナリ:0 01111110 00000000000000000000000

ここで確認してください: http://www.binaryconvert.com/result_float.html?decimal=04804605

浮動小数点数がどのように保存されるかを既に理解している場合。数値が32ビット(単純精度)で保存できない場合はどうなりますか。

例:10進数。 1/3 = 0.3333333333333333333333無限であるため、データを保存するための5ビットがあると仮定します。繰り返しますが、これは現実的ではありません。ただ考えてみてください。したがって、コンピューターに保存されるデータは次のようになります。

0.33333.

数値がロードされると、コンピューターは再び計算します。

0.33333 = 3*10^-1 + 3*10^-2 + 3*10^-3 + 3*10^-4 +  3*10^-5.

これについて:

$a = '35';
$b = '-34.99';
echo ($a + $b);

結果は0.01(10進数)です。この番号をバイナリで表示します。

0.01 (decimal) = 0 10001111 01011100001010001111 (01011100001010001111)*(binary)

ここで確認してください: http://www.binaryconvert.com/result_double.html?decimal=048046048049

(01011100001010001111)は1/3のように繰り返されるためです。そのため、コンピューターはこの番号をメモリに保存できません。犠牲にしなければなりません。これはコンピューターの精度ではありません。

高度な(数学に関する知識が必要です)では、0.01ではなく10進数で簡単に表示できるのはなぜですか?.

0.01(10進数)の2進数の小数部が有限であるとします。

So 0.01 = 2^x + 2^y... 2^-z
0.01 * (2^(x+y+...z)) =  (2^x + 2^y... 2^z)*(2^(x+y+...z)). This expression is true when (2^(x+y+...z)) = 100*x1. There are not integer n = x+y+...+z exists. 

=> So 0.01 (decimal) must be infine in binary.
1

number_format(0.009999999999998, 2)または$res = $a+$b; -> number_format($res, 2);を使用する方が簡単ではないでしょうか?

0