浮動小数点精度演算が不正確な原因

5478 ワード

なぜ浮動小数点精度演算に問題があるのか
私たちが普段使っているプログラミング言語の多くには、浮動小数点型の精度演算が正確ではないという問題があります.たとえば
double num = 0.1 + 0.1 + 0.1;
//       0.30000000000000004
double num2 = 0.65 - 0.6;
//       0.05000000000000004

筆者はテスト中にC/C++にこのような問題が起こらないことを発見し、最初はコンパイラの最適化だと思って、この問題を解決しました.しかしC/C++他の言語を解決できればなぜフォローしないのですか?この問題の発生原因から見ると,コンパイラ最適化はこの問題を解決するのに論理的に通じない.その後、印刷方法に問題があることがわかり、印刷出力方法は四捨五入されます.使用printf("%0.17f
", num);
およびcout << setprecision(17) << num2 << endl;数桁の小数を多く印刷すると精度演算が不正確な問題が見られます.
では、精度演算が正確ではないのはなぜでしょうか.次に、コンピュータのすべてのデータの表現形式のバイナリから話す必要があります.バイナリと10進数の相互変換をよく知っていれば、精度演算が不正確な問題の原因が何なのか簡単に知ることができます.知らなかったら、十進法とバイナリの相互変換の流れを振り返ってみましょう.一般的にバイナリから十進法に移行して使用しているのは です.十進法転入バイナリは 2 , です.よく知っている同級生は省略することができる.
//        
10010 = 0 * 2^0 + 1 * 2^1 + 0 * 2^2 + 0 * 2^3 + 1 * 2^4 = 18  

//        
18 / 2 = 9 .... 0 
9 / 2 = 4 .... 1 
4 / 2 = 2 .... 0 
2 / 2 = 1 .... 0 
1 / 2 = 0 .... 1

10010

では、問題は10進数小数と2進数小数がどのように互いに変換されているのか.10進数小数から2進数小数は一般的に 2 , 2 , です.2進数小数から10進数までは使用 .
//        
10.01 = 1 * 2^-2 + 0 * 2^-1 + 0 * 2^0 + 1 * 2^1 = 2.25

//        
//     
2 / 2 = 1 .... 0
1 / 2 = 0 .... 1
//     
0.25 * 2 = 0.5 .... 0 
0.5 * 2 = 1 .... 0 

//    10.01

回転小数点も理解しました.次に本題に戻ります.なぜ浮動小数点演算に精度が不正確なのかという問題があります.次に、簡単な例2.1という10進数を2進数に変換するのはどのようなものかを見てみましょう.
2.1      
//     
2 / 2 = 1 .... 0
1 / 2 = 0 .... 1

//     
0.1 * 2 = 0.2 .... 0
0.2 * 2 = 0.4 .... 0
0.4 * 2 = 0.8 .... 0
0.8 * 2 = 1.6 .... 1
0.6 * 2 = 1.2 .... 1
0.2 * 2 = 0.4 .... 0
0.4 * 2 = 0.8 .... 0
0.8 * 2 = 1.6 .... 1
0.6 * 2 = 1.2 .... 1
0.2 * 2 = 0.4 .... 0
0.4 * 2 = 0.8 .... 0
0.8 * 2 = 1.6 .... 1
0.6 * 2 = 1.2 .... 1
............

無限ループに落ちる結果は10.000100110011......我々のコンピュータは小数を格納する際に長さに制限があるに違いないので、小数の一部を切り取って格納し、コンピュータが格納する数値は正確な値ではなく、大まかな値にすぎない.ここから見ると、私たちのコンピュータは2.1という10進数の値を正確に表すためにバイナリを使うことができず、表示さえ正確に表すことができず、計算に問題があるに違いない.
精度演算損失の解決方法
既存には3つの方法がある
  • 業務が非常に正確でなければならない要件でなければ、この問題を無視するために四捨五入する方法を採用することができる.
  • 整数にして計算する.
  • BCDコードを用いてバイナリ小数を格納・演算する(興味のある方は自己検索学習可能).

  • 一般的には、Pythonのdecimalモジュール、JavaのBigDecimalなど、高精度演算の解決策が用いられていますが、小数を文字列に変換して構造に伝達しなければなりません.そうしないと穴があります.他の言語は自分で探すことができます.
    # Python   
    from decimal import Decimal
    
    num = Decimal('0.1') + Decimal('0.1') + Decimal('0.1')
    print(num)
    
    // Java   
    import java.math.BigDecimal;
    
    BigDecimal add = new BigDecimal("0.1").add(new BigDecimal("0.1")).add(new BigDecimal("0.1"));
    System.out.println(add);
    

    拡張:浮動小数点の詳細
    浮動小数点型の記憶には制限がある以上、私たちのコンピュータが浮動小数点型をどのように記憶しているかを見てみましょう.本当に私たちが上述したように小数点の長さの制限があるのではないでしょうか.では、Floatのデータストレージ構造について、IEEE標準浮動小数点型によってシンボルビット、指数ビット、末数ビットの3つの部分に分けます(各部分のサイズの詳細は下図を参照).
    一般的には、大きな数または小さい数を表すには、科学的な記数法が一般的に使用されます.例えば、1000.0000001*10^3、または0.0000001*10^4と一般的に表されます.
    シンボルビット
    0は正、1は負
    しすうビット
    指数は興味深い正負を表す必要があるので、EXCESSというシステムを作りました.このシステムはどういう意味ですか?最大値/2-1は指数が0であることを示します.単精度浮動小数点型を用いて例を挙げると,単精度浮動小数点型指数位は全部で8ビットで,表す10進数は最大255である.では255/2-1=127127は指数が0であることを表す.指数ビットに格納された10進数データが128の場合、指数は128〜127=1であり、126の場合、指数は126〜127=−1である.
    端数ビット
    例えば上記の例では1.0000001および1.001が末尾に属するが、なぜ末尾と呼ぶのか.バイナリでは例えば1.xxという小数点は、小数点の前の1は永遠に存在し、保存しても空間を浪費するよりも小数点を1桁多く保存するほうがいいので、末尾の数位は小数点の部分しか保存されません.このようなデータは、上記の例の00000001および001に格納されている.
    小さな例を挙げましょう.単一精度浮動小数点型1.25の特定のストレージ構造を表示します.以下のPythonコードを使用して、floatを格納する具体的なデータ構造を表示できます.
    import struct
    
    num = 1.1
    bins = ''.join('{:0>8b}'.format(c) for c in struct.pack('!f', num))
    print(bins)
    

    Javaバージョンのコードが少し長いので、リンクを置きます.Java版コードリンク
    上記のプログラムで得られた1.25を格納するfloatバイナリ構造の具体的な値は0011111101000000000000であり、0をシンボルビットに分割して正の値である.01111111はインデックスで、01000000000000000000は端数です.次に、01111111から十進法に移行すると127であり、計算された指数は0であることを検証する.端数は0,010,000000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000
    転載先:https://juejin.im/post/5d009d63518825607160cd1c