PHPとIEEE754: 浮動小数点数を理解する

2023/07/01に公開

はじめに

私がSQL ServerとPHPで開発を行っていた際、次のような課題がありました。それは、「整数部が最大15桁、小数部が最大10桁」の要件を満たすために、「999999999999999.1234567891」のような数値をDecimal(25,10)型のカラムに保存するというものでした。しかし、このシンプルな要件にも関わらず、予期しない問題が生じました。

PDOException: SQLSTATE[42000]: [Microsoft][ODBC Driver 18 for SQL Server][SQL Server]データ型 nvarchar を numeric に変換中にエラーが発生しました。

このエラーメッセージによれば、nvarchar型をnumeric型に変換する際に問題が発生したようです。私はまずカラムの型定義を誤っているのではないかと思いました。しかし、詳しくログを調べてみると、問題は原因は別の箇所にありました。

実際の原因は、該当の数値がPHP側で「1.0E+15」という形に変換される、つまり科学的記数法(指数表記)になっていたためでした。この現象は、PHPがデフォルト設定で採用している浮動小数点数の表現形式、IEEE754が関係していると考えられました。
そこでこの記事では、この問題を解決するために必要な、IEEE754の理解に焦点を当てて解説を行います。
この記事内で誤った表現や理解を見つけた場合は、ぜひご指摘ください。

浮動小数点数の基本

この問題の理解に必要な浮動小数点数と科学記数法の基本概念について見ていきましょう。一般的に私たちが日常的に使用する「10進数」を例に取ると、次のように科学記数法で表現することが可能です。

1000 = 1 × 10^{3}\\ 0.01 = 1 × 10^{-2}

上の式を例に、「1」が仮数部、「3」や「-2」が指数部、そして「10」が基数となります。

仮数部 × 基数^{指数部}

この科学記数法の考え方をベースに、「浮動小数点数」を解説します。その名前が示す通り、浮動小数点数は「小数点の位置が動的に変わる」ことで、幅広い範囲の数値を表現できます。そして、その表現方法は基本的に科学記数法と同じで、ただし基数が「10」から「2」に変わります。つまり、浮動小数点数は数値を次の式で表現します。

仮数部 × 2^{指数部}

ここで、問題のPHPと浮動小数点数について話を戻しましょう。PHPでは数値を表現する方法としてIEEE 754形式を採用しています。IEEE 754は浮動小数点数の表現のための国際標準で、その中で科学記数法のような表現を利用し、精度と範囲のバランスを取る形になっています。

これにより、指数部が正の場合、非常に大きな数値を、また指数部が負の場合、非常に小さな数値を表現できます。これは、IEEE 754形式が有限のビット数で数値を表現しなければならないための仕組みです。

この特性が重要となるのは、例えば「9999999999999999」(16桁)のような数値を扱う場合です。この数値は、内部的には「1.0E+16」に変換されます。なぜなら、16桁の数値はPHPのデフォルトの整数範囲を超えるため、科学的記数法によって表現される浮動小数点数として扱われるからです。

IEEE754の基本

IEEE 754とは、浮動小数点数の表現方法を標準化した規格です。これにより、数値は「符号部」「指数部」「仮数部」の3つの部分に分けて表されます。PHPではこの規格のうち、倍精度(double precision)フォーマットが使用されます。

倍精度フォーマットは64ビットを使って数値を表現します。その内訳は、1ビットが符号部、11ビットが指数部、残りの52ビットが仮数部となっています。これにより、約16桁の10進数の精度を持つことができます。

しかし、実際にはすべての10進数を正確に表現することはできません。例えば次のPHPコードを見てみましょう。

<?php

// 15桁の精度を持つ浮動小数点数
echo (float)123456789012345, PHP_EOL;
// 結果 1.2345678901235E+14

このコードでは、15桁の数値を倍精度の浮動小数点数として表現しようとしています。しかし、その全ての桁を正確に表現することはできず、最後の桁は丸められ、結果として「1.2345678901235E+14」と表示されます。これは、IEEE 754が丸め誤差を最小限に抑えるための「丸めモード」を定義しているからです。

さらに、IEEE 754では、特殊な値(NaN、無限大など)を定義し、例外的な状況を取り扱うことができます。私自身、PHPでこれらの値を取り扱ったことがなかったので驚きましたが、以下のコードで確認することができました。

<?php
// NAN
$nan = acos(2); // acos(2)は未定義なのでNaNになる
var_dump($nan); // NAN
var_dump(is_nan($nan)); // true

// 無限大
$inf = log(0); // log(0)は無限大になる
var_dump($inf); // -INF
var_dump(is_infinite($inf)); // true

問題解決のための解法

数値の扱いに問題が生じた際の最も簡単な解法は、数値を文字列として扱うことです。PHPでは文字列は任意の長さを持つことができ、そのため大きな数値もそのままの形で表現することが可能です。この文字列をそのままSQL Serverに渡すことで、数値が科学的記数法に変換されることなく、そのままデータベースに保存することができます。

ただし、文字列として扱うと、数値演算ができないという制約があります。それゆえ、数値としての演算が必要な場合には、適切なタイミングで型変換を行うことが必要です。

今回の問題は、データを正確にデータベースに保存することが求められていたため、数値を文字列として扱うという方法で解決しました。確かに本来あるべき型とは異なる型でデータを扱うことには違和感がありましたが、この問題を解決するために妥協しました...。

その他の解決策としては、PHPにはGMPやBCMathといった任意精度数学関数ライブラリが存在します。これらのライブラリを使用すると、任意の長さと精度の数値を正確に扱うことが可能になります。しかし、これらのライブラリはPHPの標準機能ではないため、利用するには適切な設定が必要となります。私自身、これらのライブラリを用いた経験はありませんが、同様の問題に遭遇した場合には参考になるかもしれません。

まとめ

コンピュータ上で数値を扱う際、固定小数点や整数といった形式に加えて、広範囲の数値を精度良く表現するためには浮動小数点数が不可欠です。そのため、IEEE 754という浮動小数点数の規格を理解することは、プログラムが正確に動作するための重要な要素となります。

具体的には、IEEE 754の浮動小数点数は仮数部と指数部によって数値を表現します。これにより、大きな数値や非常に小さな数値も表現できるようになっています。しかし、これは同時に精度と範囲のトレードオフを生むため、特定の数値がどのように表現されるのか、そしてそれがどのような結果をもたらすのかを理解することは重要になると考えられます。

また、浮動小数点数が科学的記数法に変換されるメカニズムを理解することは、今回のような問題を解決する上でも重要となります。特に、PHPでは浮動小数点数がIEEE 754形式で表現され、大きな数値は科学的記数法に変換されます。これを理解していないと、期待と異なる結果をもたらすことがあります。この事実は、私が直面した問題を生じさせる主要な要素でした。

そうしたことを考えると、IEEE 754の標準規格や浮動小数点数の性質を理解することは、プログラムが正確に動作するために必要不可欠な要素となります。特に、大きな数値や小数点以下の数値を扱う場合には、浮動小数点数の特性を理解し、それに適したプログラムを作成することを意識しましょう。

参考URL

GitHubで編集を提案

Discussion