本文へスキップ
AZ Tools

プログラムを通ると数値が変わる理由

数値はシステム間で受け渡すのにもっとも安全そうに見えて、実際にはもっともよく、わずかに狂った状態で届くものです。理由は、ふつう使われる数値型が倍精度、すなわち53ビットの精度を持つ二進の分数だからです。ところが人が気にする値は十進の小数と長い識別子であり、この形式はどちらも正確には保持できません。しかも表示が誤差を丸めて消してしまうので、失敗は静かに起こります。

十進の小数は二進法には存在しない

三分の一は十進法では正確に書けません。0.3333と書いてどこかでやめるだけです。二進法では十分の一に同じことが起きます。0.1にもっとも近い保存値は0.1000000000000000055511151231257827021181583404541015625で、0.2も同じくわずかにずれているため、足し合わせると0.30000000000000004と表示される値に着地します。

これは特定の言語の不具合ではありません。同じ64ビットの形式を使う環境はすべて同じ答えを返します。その答えが形式そのものの性質だからです。二進法で正確なのは整数と、分母が2のべき乗である分数だけなので、二分の一や四分の一は完璧に振る舞い、十分の一は決してそうなりません。

九千兆を超えると整数は連続でなくなる

倍精度は2の53乗、すなわち9007199254740992まで整数を正確に保存します。そのひとつ上はまったく表現できません。9007199254740993を渡すと、黙って9007199254740992が返ってきます。この点より先では、表現できる整数の間隔が2になり、次は4になり、と広がっていきます。

その境目は、よく使われるデータ型のちょうど真ん中にあります。64ビットの識別子――データベースの主キー、大規模サービスのメッセージや投稿のID、スノーフレーク方式のID――は日常的にその値を超えるので、倍精度を通ったIDは隣の数値になって出てきます。見た目はIDのままで、どこも指していない値です。

たいていはJSONで起きる

JSON自体は数値の大きさを制限していません。形式はテキストで、桁はすべてそこに書かれています。桁を失わせるのはパーサーのほうで、多くのパーサーは読み込むそばからすべての数値を倍精度に変えてしまいます。したがって壊れるのは受け取る側であり、送った側のログは完璧に見えます。

実務的な規則は、大きな識別子を文字列として運ぶことです。IDは数量ではありません。二つを足すことなど決してないので、引用符で囲んでも失うものはなく、任意のパーサーを往復して生き残る方法はそれだけです。送るものを変えられないなら、せめて受け取ったIDを、解析後の数値ではなく元のテキストと突き合わせてください。

お金がもうひとつの噛みつく場所

価格は十進の小数なので、ここまでの問題をそのまま受け継ぎます。十分の一は正確ではなく、十分の一の三倍は十分の三ではなく、長い合計は一円の何分の一かずつずれていき、やがて目に見える差として丸められます。お金は最小単位の整数として持つか、そのために作られた十進型に入れてください。

丸めは別の判断であり、人はそれが世界共通だと思い込みますが、そうではありません。0.5を切り上げるやり方と、0.5を近いほうの偶数へ寄せるやり方は、どちらも会計で長い歴史があり、言語や表計算ごとに既定値が違います。あらゆる入力が一致している二つのシステムが、合計で食い違うのはこのためです。

表示が損傷を隠す

倍精度の値を表示するとき、ふつうは同じ値として読み戻せる最短の表記が使われます。そのため、わずかに狂った数値もたいていは期待どおりのきれいな値として印字されます。二つの数値がまったく同じに表示されながら等値の判定に失敗することがあり、この種の不具合が超常現象めいて感じられる状況がまさにこれです。

ですから同じ土俵で比べてください。実際に保存されている値を見るには有効数字十七桁で出力するか十進数に変換し、計算した二つの値を比べるときは、完全一致を問う代わりに大きさに見合った許容差を置いてください。

十分な桁を書かなければ往復で失われる

倍精度をテキストにして戻す往復が正確であるためには、有効数字十七桁が必要です。32ビットの単精度なら九桁です。読みやすさのために小数点以下六桁で整える処理――ログの一行、CSVの書き出し、設定ファイル――は値の一部を捨てており、それを読み戻すと出発点とは違う数値になります。

意図して精度を落とす場合も同じです。32ビットの単精度は有効数字およそ七桁しか保持しないので、倍精度の値をそこに通して戻すと0.1は0.100000001490116119384765625になります。ゲームの座標なら問題なく、これから百万回足し合わせる測定値なら問題です。

数値がおかしいときに確かめること

驚かされるまで待たずに、境界をわざと試してください。0.1足す0.2をその経路に通して余分な桁が現れるかを見る。9007199254740993を送ってそのまま返るかを見る。有効数字十七桁を超える値を送って何が残るかを見る。

食い違いを見つけたら、どの段階が情報を落としたのかをまず切り分けてください。表示か、パーサーか、単精度への変換か、演算そのものか。それぞれ直し方が違います。桁を増やす、IDを引用符で囲む、より広い型を使う、整数に移す。的外れな手当てをすると、問題はそのまま残ったうえに、いっそう見えにくくなります。

関連ツール