【ソースコード解析シリーズ】number-precision と bignumber.js

01_JS精度

かなり前に社内で共有した記事を今だけ公開します... この記事では、0.1 + 0.2 != 0.3 となる理由を説明し、number-precision と bignumber.js の解決原理を分析します。

JSの精度の問題でつまづいていたので、復習と学習のためにシステムが来ました~

バックグラウンド

実際の事業開発では、以下のような問題に遭遇することがあります。

// 加法
0.1 + 0.2      // 0.30000000000000004

// 减法
1.5 - 1.2      // 0.30000000000000004

// 乘法
19.9 * 100     // 1989.9999999999998

// 除法
0.3 / 0.1      // 2.9999999999999996

toFixed()必要に応じてtoPrecision()切り上げます

この問題を解決するために時々使用しますtoFixed()が、実際、この方法では望ましくない結果が生じることがあります。

2.54.toFixed(1)         // 2.5
2.56.toFixed(1)         // 2.6

2.55.toFixed(1)             // error: 2.5
2.55.toPrecision(1)         // error: 2.5

業界では古典的な面接の質問が生まれました。「なぜ 0.1 + 0.2 は 0.3 に等しくないのですか?」

IEEE 754JS は倍精度バージョン (64 ビット) を使用するため、IEEE 754使用される言語に限りこの問題が発生します。

IEEE754

予備知識

  • コンピュータの内部はバイナリ、つまり0 1コードの構成で表現されます。

  • 10 進数を 2 進数に変換します。

    • 正の整数からバイナリへの変換: 正の整数を 2 で割り、得られた商を商が 0 または 1 になるまで再度 2 で割り、余りを逆方向にリンクします。その後、上位ビットを 0 (8 ビットの場合) で埋め、前に 2 つの 0 を追加します。したがって、最終結果は次のようになります
      ここに画像の説明を挿入

    00100110 0010 011000100110

    • 負の数を 2 進数に変換します。まず正の整数を 2 進数に変換し、次に 2 進数を反転して、結果に 1 を加算します。

      -38例として、バイナリ380010 0110 、反転後の結果は1101 1001、1 を加算した後の結果は です1101 1010

    • 10 進数を 2 進数に変換する: 小数点以下の数値に 2 を掛け、整数部分を取り出し、次に小数部分に 2 を掛け、小数部分が 0 になるか桁数が OK になるまで順番にプッシュし、10 進数の 2 進数の結果を取得するために整数部分を配置します。

      例として 0.125 を取り上げます。

      0.125 * 2 = 0.25 --------------- 取整数 0,小数 0.25
      0.25 * 2 = 0.5 ----------------- 取整数 0,小数 0.5
      0.5 * 2 = 1 -------------------- 取整数 1
      
      

    その結果、0.001必要に応じて下位ビットを 0 で埋めることができます。

    • 小数点の整数部が 0 より大きい場合は、整数部と小数点を順番に 2 進数に変換してから加算すれば OK です。したがって、2 進数の 38.125 は次のようになります。0010 0110.001
  • 科学的表記法、まず 10 進科学的表記法を例に挙げます。

    • 23.32 => 0.2332 => 小数点が左に 2 桁移動されるため、最終結果は次のようになります。

      0.2332 * 1 0 2 0.2332 * 10^20.23321 02
      バイナリは、バイナリ科学表記法で保存されます。バイナリ科学表記法である場合は、次のようになります。

    • 10111=> 1.0111=> 小数点は左に 4 桁移動され、4 を 2 進数に変換すると 100 となるため、最終結果は 1.0111 ∗ 2 ( 100 ) 1.0111 * 2^(100) となります
      1.01112( 100)

二項科学技術法則によれば、小数点の前にはゼロ以外の値がなければなりません

IEEE754とは

IEEE754 規格では次のように規定されています。

  • float単精度浮動小数点数は機械内で数値の記号を1ビットで表現し、指数は8ビット、仮数は小数部の23ビットで表現します。
  • double倍精度浮動小数点数の場合、符号を 1 ビット、指数を 11 ビット、仮数を 52 ビットで表し、指数フィールドを指数コードと呼びますすべての数値計算と比較は 64 ビットの形式で実行されます

ここに画像の説明を挿入

ではJS、すべてが倍精度浮動小数点数として格納されますNumber64bit

記号S

コンピュータではすべてが2進数で表現されているため、記号を理解するには最上位ビットを符号ビットとして0が+、1が-を意味するのが一般的です。

インデックスE

11 ビットを占めるため、値の範囲は 0 ~ 2 の 11 乗、つまり 0 ~ 1024 ビットとなり、1024 個の数値を表現できます。ただし、IEEE 754 標準では、指数オフセットの固定2 e − 1 − 1 2^{e-1}-1であると規定されています。2e 11、倍精度浮動小数点数を例にとります:2 11 − 1 − 1 = 1023 2^{11-1}-1=1023211−1 _ _1=1023

IEE754 浮動小数点数標準の 64 ビット浮動小数点数の指数オフセットが 1023 であるのはなぜですか?

ここに画像の説明を挿入

32 ビット浮動小数点数を例にとると、指数は 8 ビット、つまり 0-2 の 8 乗、つまり 256 を占めます。指数にもプラスとマイナスがあるので、-128~+128と真ん中から分かれていますが、真ん中に0があるので、-128~127までの256個の数字を表します。

ポジティブとネガティブを記録するにはどうすればよいですか? 1 つの方法は、上位を 1 に設定することで、上位が 1 であることがわかる限り、それが負の数であることがわかります。いわゆる上位 1 は、0 ~ 255 の数値を半分に分割し、0 ~ 127 は正の数を表し、128 ~ 255 は負の数を表します。しかし、このアプローチでは問題が発生します。130 と 30 などの 2 つの数字を比較した場合、どちらが大きいでしょうか? マシンは 130 の方が大きいと認識しますが、実際には 130 は負の数であり、30 より小さい必要があります。

そこで、後に誰かがすべての数値に 128 を加算して、-128 + 128 = 0、127 + 128 = 255 とすることを提案しました。このように比較すると、負の数が正の数より大きいケースはありません。

したがって、0 を読み取って 128 を減算すると、負の指数 -128 が得られ、255 を読み取って 128 を減算すると、127 が得られます。

では、なぜ最終的な指数オフセットが 128 ではなく 127 なのかというと、0 と 255 という 2 つの数値は指数を表すことができないからです。数字が 2 つ足りないため、使用できるのは 127 のみです。

同様に、64 ビット、指数 11 ビット、つまり 2^11 = 2048、1024 の半分から 0 と 2048 が削除されるため、オフセットは 1023 になります。

仮数M

仮数部 M については、次の小数部のみが保存されます。これは、1 ≤ M < 2 であるため、M がコンピュータ内に保存されるとき、この数値の最初の桁はデフォルトで常に 1 になるため、四捨五入することができ、これを行うことの利点は有効数字 1 つを節約できることです。倍精度 64 ビット浮動小数点数の場合、M は 52 ビットで、最初の 1 は四捨五入され、保存できる有効な数値の数は 52 + 1 = 53 ビットになります。

二進法則によると、小数点の前に非ゼロがなければなりません。その場合、有効なドメインは 1.xxxx です。小数点の前の 1 はデフォルトで存在しますが、デフォルトではピットを占有せず、仮数部には小数点以降の部分が格納されます

10進数をIEEE754に変換

これを使用するとNumber、コンピュータの最下層が、入力した 10 進数を IEEE754 標準浮動小数点数に自動的に変換します。

0.1 を例にとると、2 進科学表記法への変換は次のようになります。

0.1001100110011001100110011001100110011001100110011001 ※2 − 4 0.1001100110011001100110011001100110011001100110011001*2 ^{-4}0.100110011001100110011001100110011001100110011001100124

  • 0.1 は正の数であるため、符号ビットは 0 です。
  • 指数は -4 で、-4 +1023 = 1019、2 進数に変換すると 1111111011、合計 10 ビットになります。インデックス E は 11 ビットなので、上位ビットは 0 で埋められます。最終的に、01111111011 が得られます。
  • 仮数は最大 52 ビットを格納できるため、0 に丸められます。
    11001100110011001100110011001100110011001100110011001 // M 舍去首位的 1,得到如下
    1001100110011001100110011001100110011001100110011001  // 01 入,得到如下:
    1001100110011001100110011001100110011001100110011010  // 最终存储
    

0丸め方式:仮数部を右シフトした場合、取り除いた最上位桁を0として四捨五入、取り除いた最上位桁を1とし、最後の桁に1を加算

したがって、最終的な変換結果 0.1 は次のようになります。

S  E            M
0  01111111011  1001100110011001100110011001100110011001100110011010 

同様に、最終的な変換結果 0.2 は次のようになります。

S  E            M
0  01111111100  1001100110011001100110011001100110011001100110011010 // 0.2

浮動小数点数の演算

逆の順序

決済の前に、2つの数値の指数が同じか、つまり小数点の位置が一致しているかを判断する必要があります。0.1 の順序コードは -4 で、0.2 の順序コードは -3 です。小さい順序を大きい順序に揃える原則に従って、0.1 をシフトする必要があります。つまり、仮数は 1 ビット右に移動され、指数は +1 になります。

// 0.1 移动之前
0  01111111011  1001100110011001100110011001100110011001100110011010 

// 0.1 右移 1 位之后尾数最高位空出一位,(0 舍 1 入,此处舍去末尾 00  01111111100   100110011001100110011001100110011001100110011001101(0) 

// 0.1 右移 1 位完成
0  01111111100  1100110011001100110011001100110011001100110011001101

ps は最上位ビット値を変更せず、1 を補う場合は 1、0 を補う場合は 0 となります。仮数部には最上位ビットが1であることを隠しています。

仮数和

  0  01111111100   1100110011001100110011001100110011001100110011001101 // 0.1 
+ 0  01111111100   1001100110011001100110011001100110011001100110011010 // 0.2
= 0  01111111100 100110011001100110011001100110011001100110011001100111 // 产生进位,待处理

正規化と丸め

キャリーの生成により、指数コードは + 1 である必要があるため、01111111101 となり、対応する 10 進数は 1021、1021 - 1023 = -2 となるため、次のようになります。

  S  E
= 0  01111111101

最後に 2 ビットをキャリーし、最上位ビットのデフォルトの 1 を削除します。最下位ビットは 1 であるため、丸める必要があります (2 進数では 0 で終わります)。丸め方法は最下位ビットに 1 を追加し、0 の場合は直接破棄し、1 の場合は 1 を追加し続けます。

  100110011001100110011001100110011001100110011001100111 // + 1
=  00110011001100110011001100110011001100110011001101000 // 去除最高位默认的 1
=  00110011001100110011001100110011001100110011001101000 // 最后一位 0 舍去
=  0011001100110011001100110011001100110011001100110100  // 尾数最后结果

IEEE 754 の最終的なストレージは次のとおりです。

S  E           M
0  01111111101 0011001100110011001100110011001100110011001100110100

IEEE754を10進数に変換する

式によると:

n = ( − 1 ) s ∗ 2 ( e − 1023 ) ∗ ( 1 + f ) n = (-1)^s * 2^(e-1023)*(1+f)n=( 1 )s2( e1023 )( 1+f )

( − 1 ) 0 * 2 ( − 2 ) * ( 1 + 0011001100110011001100110011001100110011001100110100 ) (-1)^0 * 2(-2) * (1 + 0011001100110011001 100110011001100110011001100110100)( 1 )02 ( 2 )( 1+0011001100110011001100110011001100110011001100110100 )

最終的な答えは次のとおりです。

0.30000000000000004

印刷すると、バイナリが 10 進数に変換され、10 進数が文字列に変換されて、最終的に出力されます。10 進数から 2 進数への変換では近似が発生し、2 進数から 10 進数への変換でも近似が発生します。出力される値は実際には近似値であり、浮動小数点数の記憶内容を正確に反映しているわけではありません。

JavaScript はどのようにして 0.1 をこれほど正確に出力するのでしょうか?

精度の低下

  • 10 進数から 2 進数へ。10 進数が無限ループの場合、52 ビットを超える場合は四捨五入されます。
  • 計算に浮動小数点数が含まれる場合、順序を修正する必要があります。加算を例にとると、小さな指数フィールドを大きな指数フィールドに変換する必要があります。つまり、小さな指数浮動小数点数の小数点を左に移動すると、必然的に 52 ビットの有効フィールドの右端のビットが絞り出されます。このとき、絞り出された部分も「四捨五入」されます。ここでも精度の低下が発生します。

ソリューション

  • parseFloat結果を指定された精度に丸めますが、保守的ではありません
    210000 * 10000  * 1000 * 8.2                   // 17219999999999.998
    parseFloat(17219999999999.998.toFixed(12));    // 17219999999999.998
    parseFloat(17219999999999.998.toFixed(2));     // 而正确结果为 17220000000000
    
    
  • 浮動小数点数を整数演算に変換し、その結果を除算します。現時点では、ほとんどのシナリオに十分な考え方は、小数を整数に変換し、整数の範囲内で結果を計算し、その結果を小数に変換することです。範囲があるため、この範囲内の整数は IEEE754 浮動小数点形式で正確に表現できます
    0.1 + 0.2                        // 0.30000000000000004
    (0.1 * 100 + 0.2 * 100) / 100    // 0.3
    
  • 浮動小数点数を文字列に変換して、実際の演算プロセスをシミュレートします。

一般的な車輪

数値精度

https://github.com/nefe/number-precision

使用法

import NP from 'number-precision'
NP.strip(0.09999999999999998); // = 0.1
NP.plus(0.1, 0.2);             // = 0.3, not 0.30000000000000004
NP.plus(2.3, 2.4);             // = 4.7, not 4.699999999999999
NP.minus(1.0, 0.9);            // = 0.1, not 0.09999999999999998
NP.times(3, 0.3);              // = 0.9, not 0.8999999999999999
NP.times(0.362, 100);          // = 36.2, not 36.199999999999996
NP.divide(1.21, 1.1);          // = 1.1, not 1.0999999999999999
NP.round(0.105, 2);            // = 0.11, not 0.1

原理

主なことは、parseFloat()小数から整数への変換を組み合わせることです。加算を例に挙げます。

function plus(...nums: numType[]): number {
    
    
  // 如果是多个参数,则递归相加
  if (nums.length > 2) {
    
    
    return iteratorOperation(nums, plus);
  }

  const [num1, num2] = nums;
  // 取两个数当中,小数位长度最大的值的长度
  const baseNum = Math.pow(10, Math.max(digitLength(num1), digitLength(num2)));
  // 把小数都转为整数然后再计算
  return (times(num1, baseNum) + times(num2, baseNum)) / baseNum;
}
  • 2 つの数値のうち最大の小数点以下の長さを基数とします。
  • 2 つの数値を整数に変換し、加算して底で割ります。

の:

function times(...nums: numType[]): number {
    
    
  // 如果是多个参数,则递归相乘
  if (nums.length > 2) {
    
    
    return iteratorOperation(nums, times);
  }
  
  // 将每个变量转为整数并相乘
  const [num1, num2] = nums;
  const num1Changed = float2Fixed(num1);
  const num2Changed = float2Fixed(num2);
  const leftValue = num1Changed * num2Changed;
  
  // 检查是否越界,如果越界就报错
  checkBoundary(leftValue);
  
  // 获得分母,即Math.pow(10,小数长度的数量)
  const baseNum = digitLength(num1) + digitLength(num2);
  return leftValue / Math.pow(10, baseNum);
}

float2Fixed小数を整数に変換します。

function float2Fixed(num: numType): number {
    
    
  // 如果不是科学计数法,直接去掉小数点
  if (num.toString().indexOf('e') === -1) {
    
    
    return Number(num.toString().replace('.', ''));
  }
  
  // 如果是科学计数法,获得小数的长度
  const dLen = digitLength(num);
  return dLen > 0 ? strip(Number(num) * Math.pow(10, dLen)) : Number(num);
}

digitLength小数の長さを計算します。

// 常见的数字:1、0.1、2.2e-7
// 其中 2.2e-7 实际上就是指 0.00000022
            
function digitLength(num: numType): number {
  // 获取指数前后的数字
  const eSplit = num.toString().split(/[eE]/);
  // 如果 e 之前是小数,获取小数的数量 + e之后的数量
  const len = (eSplit[0].split('.')[1] || '').length - +(eSplit[1] || 0);
  // 返回小数的长度
  return len > 0 ? len : 0;
}

の助けを借りてparseFloat

function strip(num: numType, precision = 15): number {
    
    
  return +parseFloat(Number(num).toPrecision(precision));
} 

console.log(strip(0.1 + 0.2));    // 0.3

bignumber.js

https://github.com/MikeMcl/bignumber.js

同時に、この偉人はbig.jsdecimal.jsやコンピューティングに関連するその他のライブラリも作成しました。

ここに画像の説明を挿入

第一印象:なぜこんなに多いのか?? ?

ここに画像の説明を挿入

使用法

0.3 - 0.1                           // 0.19999999999999998
x = new BigNumber(0.3)
x.minus(0.1)                        // "0.2"
x                                   // "0.3"

原理

まずコンストラクターを見てください。うーん...ソース コードを見ると、実際には次のようになります。

ここに画像の説明を挿入

ここに画像の説明を挿入

let x = new BigNumber(123.4567);
console.log(x);
// { c: (2) [123, 45670000000000], e: 2, s: 1 }

let y = BigNumber('123456.7e-3');
console.log(y);
// { c: (2) [123, 45670000000000], e: 2, s: 1 }

追加の実装:

  • まず両方の数値を BigNumber型に変換します。例として 0.1 と 1.1 を取り上げます。
    {
          
           c: [10000000000000], e: -1, 1 }     // 0.1
    {
          
           c: [1, 25000000000000], e: 0, 1 }   // 1.1
    
  • 2 つの数値のうちどちらかが Yes であるかどうかを判断し NaN、あればそれを直接返しますnew BigNumber(NaN)
  • いずれかの当事者が負の場合は、減算の計算結果を呼び出します。
  • 記録 x.e、y.e、x.c、y.c:
    var xe = x.e / LOG_BASE,
            ye = y.e / LOG_BASE,
            xc = x.c,
            yc = y.c;
    
    console.log(xe, ye, xc, yc);      
    // -0.07142857142857142 0 [10000000000000] (2) [1, 25000000000000]
    // 其中LOG_BASE = 14;
    
  • xeとyeのどちらかが0かどうかを判定する場合、条件に応じて異なる値を返します。
    if (!xe || !ye) {
          
          
    
      // ±Infinity
      if (!xc || !yc) return new BigNumber(a / 0);
    
      // Either zero?
      // Return y if y is non-zero, x if x is non-zero, or zero if both are zero.
      if (!xc[0] || !yc[0]) return yc[0] ? y : new BigNumber(xc[0] ? x : a * 0);
    }
    
  • xe合計に注意してye、浅いコピーを実行しますxc
    xe = bitFloor(xe);    // -0.07142857142857142 => -1
    ye = bitFloor(ye);    // 0 => 0
    xc = xc.slice();
    
    // n | 0 有省略小数的作用
    function bitFloor(n) {
          
          
        var i = n | 0;
        return n > 0 || n === i ? i : i - 1;
     }
     
    console.log( 104.249834 | 0 ); //104
    console.log( 9.999999 | 0 );   // 9
    
  • yeによりxexcycの短辺を0-fill演算するので、このとき次のようになる。
    // [10000000000000]
    xc: [0, 10000000000000] 
    // [1, 25000000000000]
    yc: [1, 25000000000000]
    
  • xcと の長さを比較し yc、長さの長い値が に配置されていることを確認します xc
  • トラバースの追加:
    // Only start adding at yc.length - 1 as the further digits of xc can be ignored.
    for (a = 0; b;) {
          
          
       a = (xc[--b] = xc[b] + yc[b] + a) / BASE | 0;
       xc[b] = BASE === xc[b] ? 0 : xc[b] % BASE;
    }
    
  • 最後に、 normalise最終結果を統合することにより、新しい BigNumber オブジェクトが返されます。

私が学んだ数行のコードは非常に役立つと感じました。

// 将v转为整数比较的最快方法(当v < 2**31 时,比较是否是整数)
v === ~~v

// |0 直接取整数部分
function bitFloor(n) {
    
    
  var i = n | 0;
  return n > 0 || n === i ? i : i - 1;
}
console.log(0.6 | 0);     // 0
console.log(1.1 | 0);     // 1
console.log(3.6555 | 0);   // 3
console.log(-3.6555 | 0);   // -3

このライブラリと big.jsそれとの違いは、後者の API は前者ほどではなく、10 進数以外の計算をサポートしていないことです。

要約する

  • この型を使用するとNumber、コンピューターの最下層が、入力した 10 進数を IEEE754 標準浮動小数点数に自動的に変換します。
  • 浮動小数点数を変換するときに精度が失われます。これは通常、10 進数を 2 進数に変換するとき、または浮動小数点数が計算に参加し正確である必要があるときに発生します。
  • この問題を解決するには、parseFloat、浮動小数点から整数への変換、および浮動小数点から文字列への変換を使用することを検討できます。
  • 業界で有名なホイールには、number-precision、bignumber.js などがあります。前者は主に parseFloat と浮動小数点の整数への変換のアイデアを使用し、後者は最初に値を特定のオブジェクトに変換してから整数の計算を実行します。

参考


間違いがありましたらご指摘ください、読んでいただきありがとうございます〜

おすすめ

転載: blog.csdn.net/qq_34086980/article/details/131681840