PHPerKaigi 2023 • Day 1での登壇資料です。 https://phperkaigi.jp/2023/ https://fortee.jp/phperkaigi-2023/proposal/3630baf7-f540-4dba-b10a-89cdbddf62da
PHPで任意精度演算を行って 「正しい」金額計算をする方法2023年3月24日 / PHPerKaigi 2023 • Day 1合同会社テンマド 山岡広幸
View Slide
さて、突然ですが
0.1 + 0.2 = ??php > var_dump(0.1 + 0.2);
0.1 + 0.2 ≠ 0.3php > var_dump(0.1 + 0.2);float(0.30000000000000004) 🤔
自己紹介- 山岡広幸 / Twitter: @hiro_y- Webアプリケーションエンジニア- PHPは3から。最近はNode.js多め- 合同会社テンマド 代表社員
- 受託制作・コンサルティング- 各種アドバイザー、各種制作(デザイン・開発)- 例: 株式会社イノベーター・ジャパン 社外CTO- 自社サービスの運営- iruca、mimemoなど👨💻👨💻👩💻👩💻
0.1 + 0.2 が0.3 にならない世界
我々は PHP 8 の時代を生きているので
型で考えてみる
0.1の型?php > var_dump(0.1);float(0.1)php > var_dump(gettype(0.1));string(6) "double"🧐
マニュアルを見るhttps://www.php.net/manual/ja/language.types.float.php
float / double- PHPで、整数は int(integer)という型- 10進数、16進数、8進数、2進数で指定可能- 「整数以外」を表現するのに浮動小数点数を使う-float(ただし倍精度)、double- intの上限(PHP_INT_MAX)を超える整数も対象
浮動小数点数もう少しくわしく
2進数の世界- コンピューターは2進数で動いている- 0 or 1(OFF or ON)のバイナリの世界- 10進数の小数は2進数の世界で表現可能か?- 0.1(2進数)は2-1(10進数の0.5) 0.01(2進数)は2-2(10進数の0.25)
翻って、我々の世界- 10進数で物事を考えている- 部分的に他の基数も使うが、10進数が基本- 2進数をそのまま扱うのは不便すぎる- 10進数の0.1は2進数で正確に表現できない
浮動小数点数- 10進数の実数を有限桁の2進数の近似値で表現- 浮動小数点(floating-point)- -1 x 0.101010101010101 x 2-1 と -1 x 1.01010101010101 x 2-2 は同じ 符号、仮数、指数- 複数フォーマットがある(そのうちの1つがIEEE 754)
不便すぎるので- 必要な桁数の精度を得るための演算処理を実装- ただし、コストがかかるので非常に遅くなる…- そのうちの1つの方式が「任意精度演算」- 多くの環境で実装されている
ここで体験談
新卒のころの話- 新卒研修はCOBOL(その後Java)- 金融系のプロジェクトに配属- 投資信託/ファンドのパフォーマンス評価- たくさん小数点計算が必要だった
計算を何気なく実装result = value * rate;😡
Javaで十進演算するには- java.lang.BigDecimalを使う必要がある- プリミティブな型のfloatを使ってはいけない- COBOLは10進数を言語でサポート(二進化十進数)- C#ならdecimal、RubyならBigDecimal
我らがPHPの場合
言語として任意精度計算の仕組みがない 😵
だけど大丈夫https://www.php.net/manual/ja/language.types.float.php
BC Math 関数- GNU bc(Basic Calculator)からのfork- https://www.gnu.org/software/bc/- PHPを「--enable-bcmath」を付けて構築- 関数: bcadd、bccomp、bcdiv、bcmod、bcmul、bcpow、bcpowmod、bcscale、bcsqrt、bcsub
例えば足し算php > var_dump(bcadd('0.1', '0.2', 1));string(3) "0.3"php > var_dump(bcadd('0.1', '0.2', 20));string(22) "0.30000000000000000000"
GMP 関数- GNU Multiple Precision Arithmetic Library- https://gmplib.org/- PHPを「--with-gmp」を付けて構築- とてもたくさんの数学関数群
雑なベンチマーク$floatStart = microtime(true);for ($i = 0; $i < 10000000; $i++) {$result = 0.1 + 0.2;}$floatEnd = microtime(true);$bcStart = microtime(true);for ($i = 0; $i < 10000000; $i++) {$result = bcadd('0.1', '0.2', 1);}$bcEnd = microtime(true);echo 'float: ' . ($floatEnd - $floatStart) . "ms\n";echo 'bcadd: ' . ($bcEnd - $bcStart) . "ms\n";
高コストな結果# php test.phpfloat: 0.040148973464966msbcadd: 0.86464095115662ms🥺
注意: 引数は文字列で- BC Math関数に渡す引数は「文字列」で-float表記で渡すとstringにキャストされる- 例: 0.00001 は 1.0E-5 になってしまう- 数値を表す文字列ではないのでエラーになる可能性
一応、もう一つのやり方- PHPで整数の計算には誤差が発生しない- つまり、小数も整数に変換した上で計算、 小数に戻せば誤差は発生しない- 複雑になってしまうし、 絶対に間違えるのでオススメできない
【結論】BC Math関数を使おう
ただし- BC Math関数に用意されているのは 基本的な算術関数のみであることに注意- 例えば、floorやceil、roundのような関数はない- 雑にfloor関数等に渡すとfloatに変換されるので 計算誤差が発生する可能性がある
便利ライブラリ紹介- Brick\Math- https://github.com/brick/math- GMP関数、BC Math関数、手計算の各実装- 四捨五入、切り捨て/切り上げが実装されている
Brick\Math例$result = BigDecimal::of('2.5')->multipliedBy(BigInteger::of('2'))->divideBy('0.3', 3, RoundingMode::HALF_UP);
さて、余談// Google Chrome DevTools Console> 0.1 + 0.20.30000000000000004
JavaScriptも同様- 浮動小数点数、IEEE 754に準ずる- 解決方法: ライブラリを使う- https://github.com/MikeMcl/decimal.js- BigDecimalのプロポーザルあり- https://github.com/tc39/proposal-decimal
まとめ- 浮動小数点数の扱いには注意が必要- BC Math関数を使えばOK- 必要に応じてライブラリを利用して楽をしよう- フロントエンド側も配慮が必要
実は身近な小数点計算- 消費税、金利、住宅ローン…
正しい小数点計算をしていくために
任意精度演算を使っていこう(フロントもね!)
ありがとうございました!