Motomichi Works Blog

モトミチワークスブログです。その日学習したことについて書いている日記みたいなものです。

JavaScriptの浮動小数の丸め誤差について調べたことの備忘録

参照したページ

はじめに

個人的な備忘録です。

この記事に書いてあることは間違っているかもしれないので、実装はあくまで自己責任でお願いします。

過去に料金計算をJSで行う実装をする機会があって調べました。

10進数の桁数にもよりそうですが、単純な四則演算であれば、小数第何位を四捨五入や切り捨てなどで丸めるかを仕様として定義したうえで big.js を使って計算するのが無難なのかなと思ったので、最終的に big.js を使って実装をしました。

浮動小数点数の演算と丸め誤差についてよくわかっていなかったので 「0.1+0.2≠0.3」を説明できないエンジニアがいるらしい #Python - Qiita を読んでみたところ、10進数から2進数への変換、2進数から10進数への変換、正規化、指数部のバイアスなど、色々丁寧に説明されていて、個人的にはとても良かったです。

浮動小数点数丸め誤差とは

浮動小数点数型と誤差

実数を浮動小数点数すなわち有限桁数の2進数によって計算すると,どうしても誤差が生じる. それを丸め誤差という.

とあります。

10進数の小数の多くは、有限桁の2進数で完全に等しい値を表現することができず、近似値を表現することしかできないため、10進数から2進数、2進数から10進数へ変換するときに誤差が出ます。

浮動小数点数をconsole.log()をするときでも、浮動小数点数を使用した四則演算でも、2進数に変換して演算が行われるため誤差が出ることがあります。

「0.1+0.2≠0.3」を説明できないエンジニアがいるらしい #Python - Qiita で説明されている仮数が53ビット以内に収まらない場合は誤差が出るようです。

IEEE 754 について

wikipedia を参照しました。

IEEE 754 - Wikipedia

1985年にIEEEによって定められた、浮動小数点算術に関する標準規格である。

とあります。

丸め誤差が生じる演算の具体例

その1

小数計算の誤差 | TypeScript入門『サバイバルTypeScript』 を参照しました。

(0.1 + 0.2)Chromeのコンソールに入力すると、以下のスクリーンショットのようになります。

その2

(0.3 - 0.1)Chromeのコンソールに入力すると、以下のスクリーンショットのようになります。

その3

JavaScriptの浮動小数点数型の誤差をなくす | 株式会社CONFRAGE ITソリューション事業部 を参照しました。

(34.3 * 100)Chromeのコンソールに入力すると、以下のスクリーンショットのようになります。

(34.3 * 10 * 10)Chromeのコンソールに入力すると、以下のスクリーンショットのように、意図した結果が得られます。

その4

JavaScriptの浮動小数点数型の誤差をなくす | 株式会社CONFRAGE ITソリューション事業部 を参照しました。

(1051.8 / 10)Chromeのコンソールに入力すると、以下のスクリーンショットのようになります。10で割った場合でもなるんですね。

桁数の多い浮動小数点数と整数をconsole.log()してChromeで確認してみる

以下のような JavaScript のコードを書いて、Chromeの開発者ツールで確認しました。
小数部は19桁に固定したまま、整数部を18桁まで増やしていって、console.log()で出力しました。

const value01 = 2.1234567890123456789;
const value02 = 22.1234567890123456789;
const value03 = 222.1234567890123456789;
const value04 = 2222.1234567890123456789;
const value05 = 22222.1234567890123456789;
const value06 = 222222.1234567890123456789;
const value07 = 2222222.1234567890123456789;
const value08 = 22222222.1234567890123456789;
const value09 = 222222222.1234567890123456789;
const value10 = 2222222222.1234567890123456789;
const value11 = 22222222222.1234567890123456789;
const value12 = 222222222222.1234567890123456789;
const value13 = 2222222222222.1234567890123456789;
const value14 = 22222222222222.1234567890123456789;
const value15 = 222222222222222.1234567890123456789;
const value16 = 2222222222222222.1234567890123456789;
const value17 = 22222222222222222.1234567890123456789;
const value18 = 222222222222222222.1234567890123456789;
const value17int = 22222222222222222;
const value18int = 222222222222222222;

console.log("value01 ", value01.toString());
console.log("value02 ", value02.toString());
console.log("value03 ", value03.toString());
console.log("value04 ", value04.toString());
console.log("value05 ", value05.toString());
console.log("value06 ", value06.toString());
console.log("value07 ", value07.toString());
console.log("value08 ", value08.toString());
console.log("value09 ", value09.toString());
console.log("value10 ", value10.toString());
console.log("value11 ", value11.toString());
console.log("value12 ", value12.toString());
console.log("value13 ", value13.toString());
console.log("value14 ", value14.toString());
console.log("value15 ", value15.toString());
console.log("value16 ", value16.toString());
console.log("value17 ", value17.toString());
console.log("value18 ", value18.toString());
console.log("value17int ", value17int.toString());
console.log("value18int ", value18int.toString());

出力結果は以下のスクリーンショットの通りでした。

  • value17は整数部だけで17桁あり、一の位は4になっています。
  • value18は整数部だけで18桁あり、一の位と十の位が0になっています。
  • value17intやvalue18intも同様の結果が出力されているので、整数でも17桁以上あると正しく変換されないことがわかりますが、16桁の整数が全て正しく変換されることの証明にはなりません。
  • value10の出力を見ると、整数部と小数部を合わせて15桁目までは変数に格納した値がそのまま出力され、16桁目が変化しています。

浮動小数点数型と誤差 の double 型 のセクションを見ると、以下のように記載されています。

double 型の精度(有効桁数)は2進数にして 53 (=52+1) 桁であり,10進数では約 15 桁となる.
指数部も有限であるため, double で表すことのできる実数の絶対値は次のような範囲に限られる.

詳しくは 浮動小数点数型と誤差 をご確認ください。

TypeScript Deep Dive内のbig.js紹介セクション

https://typescript-jp.gitbook.io/deep-dive/recap/number#big.js

財務計算(例:GST計算、セントでのお金、追加など)のために数学を使用する場合は、big.jsのようなライブラリを使用します。
・完全無欠な10進演算
・安全な範囲外の整数値

と紹介されていて、正確な10進数の演算ができそうです。

big.jsのREADME

GitHub - MikeMcl/big.js: A small, fast JavaScript library for arbitrary-precision decimal arithmetic.

big.js, bignumber.js, and decimal.jsの違いについて

big.js の wiki に記載があります。

https://github.com/MikeMcl/big.js/wiki

google翻訳してみると、以下のようなことなどが書かれているようです。ご自身で公式ページをご確認いただくことをお勧めします。

  • big.js は、最小限の任意精度ライブラリです。 これは 3 つの中で最も単純かつ最小で、bignumber.js の半分以下のサイズで、半分のメソッドしかありません。 NaN または Infinity を正当な値として受け入れず、他の基数では機能せず、実行時設定オプションは、小数点以下の桁数と除算を伴う演算の丸めモード、および次の指数値の設定に限定されます。
  • bignumber.js と decmal.js は他の基数の値を処理でき、16 進数の「0x」などの接頭辞をサポート。
  • decmal.js は、C プログラミング言語で見られるような 2 進指数表記で 2 進数、8 進数、および 16 進数の値を処理することもできます。
  • decmal.js はもともと、bignumber.js に非整数累乗のサポートを追加することによって開発されましたが、別のライブラリとしてリリースすることにしました。
  • bignumber.js は、除算を伴う演算が使用されない限り、精度が失われることを心配する必要がない。
  • decmal.js は、非常に小さい値または大きい値をより効率的に処理できるため、より科学的なアプリケーションに適している可能性があります。
  • decmal.js は非整数累乗もサポートし、三角関数と exp、ln、log メソッドを追加します。 これらの追加により、decmal.js は bignumber.js よりも大幅に大きくなります。

10進数と2進数の対応表

10進数 2進数
0.125 0.001
0.25 0.01
0.5 0.1
0 0
1 1
2 10
3 11
4 100
5 101
6 110
7 111
8 1000

n進法の位と累乗の関係について

10進法では十の位、百の位、千の位はそれぞれ、10の1乗の位、10の2乗の位、10の3乗の位であることがわかります。
一の位から左へ位が上がるに従って、nの1乗の位、nの2乗の位、nの3乗の位となっています。

また、10進法で小数第一位、小数第二位、小数第三位はそれぞれ、10の-1乗の位、10の-2乗の位、10の-3乗の位であることがわかります。
小数点から右へ小数第一位、小数第二位、小数第三位と右へ行くに従って、nの-1乗の位、nの-2乗の位、nの-3乗の位となっています。

2進法でも同様で、繰り返しの説明となりますが、左へ位が上がるに従って、nの1乗の位、nの2乗の位、nの3乗の位となっています。
小数点から右へ小数第一位、小数第二位、小数第三位と右へ行くに従って、nの-1乗の位、nの-2乗の位、nの-3乗の位となっています。

2進数で表すことができる小数

上記した10進数と2進数の対応表からもわかる通り、2進数では 0.5、0.25、0.125 という風に、2のマイナス累乗の値と、それらを組み合わせた値しか表現できず、どこまでいっても「10進数の0.1と等しい値」が表現できないことがわかります。

10進数の 0.1 は2進数に変換すると循環小数になってしまうことが 「0.1+0.2≠0.3」を説明できないエンジニアがいるらしい #Python - Qiita で説明されています。

コンピュータ内部で使用される2進数の桁は有限であることから、一定の桁数の近似値でしか小数の計算ができず誤差が生まれます。

32ビット単精度の交換形式のビット割り当て

単精度浮動小数点数 - Wikipedia

wikipedia や、色々なところに記載がありますが、32ビット単精度の交換方式

以下のように記載があります。

IEEE 754 での binary32 の定義は次の通りである。

・符号ビット: 1ビット
・指数部の幅: 8ビット
仮数部の幅: 23ビット

  • 符号によって正の数・負の数を表しています。
  • 2進数で演算を行うので、基数は2で固定です。