LoginSignup
5
2

More than 1 year has passed since last update.

PHP で計算すると起こるわかりにくい現象

Last updated at Posted at 2022-03-12

これは何?

64bit 環境では、PHP の整数型が 64bit であることを知って驚いたので、そこに起因する不思議なことを見ていく。

基本的な動き

PHP は、整数型の値を整数型の値で割ると、整数じゃなくなることがある。

PHP8
<?php
$a=10;
$b=8;
printf( "%f/%f=%f\n", $a, $b, $a/$b); #=> 10.000000/8.000000=1.250000

そして、JavaScript と異なり、型も変わる。

PHP8
<?php
$a=10;
$b=8;
printf( "%s/%s=%s\n", gettype($a), gettype($b), gettype($a/$b)); #=> integer/integer=double

さらに。
Python3 の / と異なり、除算結果が整数だと型は double にならない。

PHP8
<?php
$a=10;
$b=2;
printf( "%f/%f=%f\n", $a, $b, $a/$b); #=> 10.000000/2.000000=5.000000
printf( "%s/%s=%s\n", gettype($a), gettype($b), gettype($a/$b)); #=> integer/integer=integer

それだけではなく。
(整数による除算では無理だけど)結果が integer で表現でる範囲を外れると double になる。

PHP8
<?php
$a=10**7;
printf( "%f**1 is %s\n", $a, gettype($a**1)); #=> 10000000.000000**1 is integer
printf( "%f**2 is %s\n", $a, gettype($a**2)); #=> 10000000.000000**2 is integer
printf( "%f**3 is %s\n", $a, gettype($a**3)); #=> 10000000.000000**3 is double

と。
ここまではまあそういう仕様なんだふーんという気分になるかもしれない。

わかりにくい現象

こんな事が起こる。

PHP8
<?php
$a = 1111111111111111110;
$b = 1111111111111111111;
$c = 1111111111111111112;
printf( "$a/2 = %d\n", $a/2); #=> 1111111111111111110/2 = 555555555555555555
printf( "$b/2 = %d\n", $b/2); #=> 1111111111111111111/2 = 555555555555555584
printf( "$c/2 = %d\n", $c/2); #=> 1111111111111111112/2 = 555555555555555556
printf( "$b<$c is %s\n", ($b<$c ? "true" : "false"));
#=> 1111111111111111111<1111111111111111112 is true
printf( "($b/2 )<($c/2) is %s\n", (($b/2)<($c/2) ? "true" : "false"));
#=> (1111111111111111111/2 )<(1111111111111111112/2) is false

1111111111111111111 は 60bit ほどあるので、その半分は 59bit ほど。
それを 2 で割った結果を整数で表現すると最寄りの値との誤差は 0.5 になるけど「整数にならないから double にするよ」という処理をされた結果、正確な値からのずれが 30ぐらいになってしまう。

結果を整数にしたいとわかっているときは intdiv という関数を使うのが良いんだけど、「より正確な値が欲しい」というニーズに応えるのは難しい。

除算以外でも

こちらは 32bit の PHP でも起きていた現象、というか、64bit だと比較的起きにくくなった現象だけど、こんな事が起こる。

PHP
<?php
$a=7777777777777777777;
$b=5555555555555555555;
$c=-5555555555555555555;

# a と b の平均
printf( "%d\n", intdiv(($a+$b), 2) ); #=> Uncaught TypeError: intdiv()
printf( "%d\n", $a+intdiv(($b-$a), 2) ); #=> 6666666666666666666 🆗 
printf( "%d\n", ($a+$b)/ 2 ); #=> 6666666666666665984 ❌

# a と c の平均
printf( "%d\n",intdiv($a+$c,2) ); #=> 1111111111111111111 🆗
printf( "%d\n", $a+intdiv(($c-$a), 2) ); #=> Uncaught TypeError: intdiv()
printf( "%d\n", $a+($c-$a)/2 ); #=> 1111111111111111680 ❌

平均を取る計算の途中で整数の範囲を超えると誤差が大きくなる。
オーバーフローを避けるためには a+(b-a)/2 を使えばいい」というのは正しくなく、上記の通りどっちもどっち。

「53bit を超えるから intdiv だよね」という対策をすると、オーバーフローでランタイムエラーになる。
まあオーバーフローで見えにくい誤差が出るよりは intdiv がいいかもね。

感想

32bit 整数を勝手に倍精度にするのは、困らない気がするけど、64bit 整数を勝手に倍精度にするのは桁落ちするのでよろしくないと思う。

64bit 整数どうしの計算結果が非整数やオーバーフローになったとき、倍精度に勝手に変換される処理系を他に知らないんだけど、どうだろう。

64bit の話とは関係ないけど、 intdivdouble を与えたら例外なのはおどろいた。てっきり勝手に整数に変換されるんだと思っていた。

そういえば文字列与えたらどうなんだろ思ったら

php8
<?php
printf( "%d\n", intdiv(200,7)); #=> 28
printf( "%d\n", intdiv("200",7)); #=> 28
printf( "%d\n", intdiv("200.0",7)); #=> 28
printf( "%d\n", intdiv("2e2",7)); #=> 28
printf( "%d\n", intdiv("2e18",7)); #=> 285714285714285714

printf( "%d\n", intdiv("2e200",7)); 
#=> Fatal error: Uncaught TypeError: intdiv(): 
#=> Argument #1 ($num1) must be of type int, string given (以下略)

こんな挙動。エラーメッセージがまちがってるね。

5
2
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
2