PHPの演算誤差

(1/1)
PHPに限った問題ではないが、コンピュータ・プログラムは内部で2進数演算しているため、10進数の計算と比べると誤差が発生することがある。そこで今回は、演算誤差の具体事例と対策を考えていく。

(2021年2月6日)PHP8対応

目次

サンプル・プログラム

循環小数による演算誤差

   9: //循環小数
  10: $a = 1;
  11: $b = 3;
  12: $x = $a / $b;
  13: printf("%s÷%s=%f<br />\n", $a, $b, $x);
  14: $x = $x * $b;
  15: printf("%s÷%s×%s=%f<br />\n", $a, $b, $b, $x);
  16: 
  17: //小数乗算
  18: $a = 10000;
  19: $b = 70.21;
  20: $x = $a * $b;
  21: printf("%s×%s=%d<br />", $a, $b, $x);
  22: printf("%s×%s=%f<br />", $a, $b, $x);
  23: 
  24: /*

1÷3=0.333333
1÷3×3=1.000000
10000×70.21=702099
10000×70.21=702100.000000
プログラムを実行すると、11行目の演算結果は 0.333333....(循環小数)となるため、結果は切り捨てになってしまう。これは表現上の問題なのでやむを得ない。商に 3 を乗算してやれば、1 に戻るので、演算誤差はないといえる。

ところが、単純な乗算プログラムなのに演算誤差が発生するケースがある。期待する結果は 702100 であるはずなのに、702099 と表示されてしまう。
だが、奇妙なことに、printf 関数の %f 修飾子(小数表示)を利用すると、702100.000000 と表示される。整数化の際に誤差が発生しているのだ。

BCMath任意精度数学関数

PHPには、BCMath任意精度数学関数という関数群が用意されている。これは、10進数表記された数字に対して任意精度の演算を提供するものである。

   9: bcscale(10);       //有効桁数を10桁にする
  10: //小数乗算
  11: $a = 10000;
  12: $b = 70.21;
  13: $x = bcmul($a, $b);
  14: printf("%s×%s=%d<br />", $a, $b, $x);
  15: 
  16: //循環小数
  17: $a = 1;
  18: $b = 3;
  19: $x = bcdiv($a, $b);
  20: printf("%s÷%s=%f<br />\n", $a, $b, $x);
  21: $x = bcmul($x, $b);
  22: printf("%s÷%s×%d=%d<br />\n", $a, $b, $b, $x);
  23: 
  24: /*

10000×70.21=702100
1÷3=0.333333
1÷3×3=0
サンプル・プログラムを実行すると、結果は、期待したとおりの 702100 となった。
まず、関数  bcscale  で有効桁数を指定する。関数  bcmul  は任意精度の乗算である。

ところが、今度は循環小数の計算で問題が起きてしまう。
除算を行う関数  bcdiv  の段階で、変数 $x には有効桁数10桁の10進数――0.3333333333――が代入される。このため、次の乗算の段階で、0.9999999999となってしまう。
関数  printf  の修飾子 %d や %f を %s に変更してもらえれば、このあたりの内部状況がよく分かるだろう。

任意精度で四捨五入

前述の循環小数問題を回避するため、丸め(四捨五入)を行うことにする。

  10: /**
  11:  * 任意精度の丸め関数
  12:  * @param string    $val 丸める数字
  13:  * @param int        $precision 丸め位置(小数点の桁数)
  14:  * @return string    結果の数字
  15: */
  16: function bcround($val, $precision) {
  17:     $x = bcdiv($val, $precision);
  18:     $x = round($x, 1);
  19:     return bcmul($x,  $precision);
  20: }

  22: bcscale(10);       //有効桁数を10桁にする
  23: //循環小数
  24: $a = 1;
  25: $b = 3;
  26: $x = bcdiv($a, $b);
  27: printf("%s÷%s=%f<br />\n", $a, $b, $x);
  28: $x = bcmul($x, $b);
  29: $x = bcround($x, 1);
  30: printf("%s÷%s×%d=%d<br />\n", $a, $b, $b, $x);
  31: 
  32: /*

1÷3=0.333333
1÷3×3=1
ここでは、任意精度を扱える bcround をユーザー定義し。
これで、期待通りの数値を得ることができるようになった。実際のプログラミングの場面では、bcround は万能ではなく、用途に応じた丸め関数を用意することになるだろう。そのときも、ここで定義した bcround 関数のコーディング方法が役に立つと思う。

参考サイト

(この項おわり)
header