演算精度 =============== ## 浮動小数点 このページでは,浮動小数点による演算の精度について学習します. コンピュータ内部では,小数点以下を含む数値を処理するために,一般的に浮動小数点型や浮動小数点演算を行います. JavaScriptでは,整数型や小数型を意識することは(あまり)有りませんが,内部的にこのような形で処理されます. 浮動小数点型というのは,m☓2^eで数値を表します. また,そのbit数で型が分かれています. IEEE754で定められている浮動小数点型とビット数を以下に示します. ここで,仮数部のbit数というのはmの部分に使えるbit数で,指数部のbit数というのはeの部分に使えるbit数です. JavaScriptの場合は,特に指定しない限り倍精度が使用されます. 型名 | 符号bit数 | 指数部bit数 | 仮数部bit数 | 総bit数 -- | --: | --: | --: | --: 半精度 | 1 | 5 | 10 | 16 単精度 | 1 | 8 | 23 | 32 倍精度 | 1 | 11 | 52 | 64 4倍精度 | 1 | 15 | 112 | 128 ## 浮動小数点と0.1 10進数では1/3を計算すると0.33333333...といつまで経っても割り切れない数(循環小数)になってしまいます. これと同じように2進数では1/3や1/5が循環小数になります. また,1/5が循環小数になるということは,1/10も循環小数になります. 人間にとって分かりやすいと考えられている1/10は,コンピュータにとってはものすごく半端な数となってしまいます. まずは0.1を,小数点以下40桁まで表示してみましょう. 0.1の後,途中までは0が並んでいますが,小数点以下17桁辺りから妙な数字が並んでいると思います. コンピュータ内部では1/10が循環小数であることと,演算に使えるbit数が有限であることから,微小な誤差(丸め誤差)が生じてしまいます. ```javascript runnable console editable // 0.1を小数点以下40桁まで表示する var x = 0.1; console.log( "何も指定せずに表示:" + x ); console.log( "小数点以下40桁まで表示:" + x.toFixed(40) ); ``` ## 微小な誤差を持つ数値の積和(1) 次に,初心者がよく陥りがちな演算の例として,0.0001を順次足していく例を示します. 例えば10000回足すと1.0になることを期待してしまいますし,100000000回足すと1000になることを期待してしまいます. しかし,微小な誤差が積もり積もると大きな誤差になってしまいます. このような場合は順次足していくのではなく,掛け算を使用することが望ましいです. これらの計算結果を実際に見てみましょう. ```javascript runnable console editable // 繰り返し足し算した結果と掛け算の結果の比較 var x = 0.00001; var y1 = 100000; var y2 = 100000000; var sum = 0.0; console.log( "初期値:" + x.toFixed(40) ); // 100000回 console.log( "\n100000回の積和"); console.log( y1 + " ☓ " + x + " = " + (x * y1).toFixed(40) ); for( var z=0; z<100000; z++ ) sum += x; console.log( "繰り返し足した結果: " + sum.toFixed(40) ); sum = 0.0; // 100000000回 console.log( "\n100000000回の積和"); console.log( y2 + " ☓ " + x + " = " + (x * y2).toFixed(40) ); for( var z=0; z<100000000; z++ ) sum += x; console.log( "繰り返し足した結果: " + sum.toFixed(40) ); ``` ## 微小な誤差を持つ数値の積和(2) 積和で誤差が出る別の例を示します. こちらも初心者が陥りやすいのですが,微小な数値を足していく処理を作り,その繰り返し条件が「0.0 になるまで繰り返す」としてしまう場合です. 誤差によって0.0にはならないため,そのような条件でループすると無限に繰り返します. 気をつけてください. ここでは,-1000に順次0.0001を加えていき,10000000回でちょうど0.0になるはずのところの演算結果を見てみましょう. ```javascript runnable console editable // 繰り返し足し算した結果と掛け算の結果の比較 var x = 0.0001; var sum = -1000.0; console.log( "初期値 x = " + x.toFixed(40) ); console.log( "初期値 sum = " + sum.toFixed(40) ); console.log( "\n10000000回の積和"); for( var z=0; z<10000000; z++ ) sum += x; console.log( "繰り返し足した結果: " + sum.toFixed(40) ); console.log( "通常目にする表記: " + sum ); ``` ## 情報落ち(1) 浮動小数点の演算では,扱える数値の範囲が広いので忘れがちですが,仮数部のbit数を超えて記憶することはできません. ここでは,大きな数と小さな数の加算結果を見てみましょう. 大きな数の例として1.0☓10^10,小さな数の例として1.0☓10^-10を足してみます. 表示の際のアンダーバー(_)は,桁揃えのために入れているので無視してください. ```javascript runnable editable console // 要素番号を指定して代入する例 var x = 1e10; var y = 1e-10; console.log( x.toFixed(20) ); console.log( "__________" + y.toFixed(20) ); console.log( (x + y).toFixed(20) ); ``` ## 情報落ち(2) 上の例では小数点以下の小さな数値を足してみましたが,今度は小数点以下を含まない例を示します. 具体的には,倍精度型の仮数部は52bitなので,53bit以降の桁は何らかの処理が施されてしまうことを示します. 大きな数の例として2^53,小さな数の例として1を足してみます. 2^53は仮数部52bitを使い切ってしまいますが,そこに1を加えると53bit目に相当するので,無視されてしまいます. ```javascript runnable editable console // 要素番号を指定して代入する例 var x = Math.pow(2.0, 53.0); var y = 1.0; console.log( x.toFixed(20) ); console.log( "_______________" + y.toFixed(20) ); console.log( (x + y).toFixed(20) ); ```