クロスプラットフォームによって引き起こされる浮動小数点のバグ|予期しない結果があります

クロスプラットフォームによって引き起こされる浮動小数点のバグ|予期しない結果があります

コードエイプストーン
クロスプラットフォームによって引き起こされる浮動小数点のバグ|予期しない結果があります

この記事は、当初はworkdpressプログラムであったため、6年前に整理されて再発行されましたが、静的なブログに変更されたため、形式が混乱しました。この古い記事は、で元のテキストをクリックするとアクセスできます。記事の終わり。

問題の背景

背景は少し単純です。最初は、プロジェクトは浮動小数点演算を含むC#で記述されていたため、インとアウトは省略されていました。次のコードを見てください。(この問題が発生する理由は、非常に奇妙な問題がオンラインで発生したためです。これは、ローカルのデバッグ効果と一致していません。)


float p3x = 80838.0f;
float p2y = -2499.0f;
double v321 = p3x * p2y;
Console.WriteLine(v321);

非常に簡単です。すぐに結果を-202014162と計算しました。問題ありません。C#はそのような結果を生成しませんでしたか?それは不可能です。VisualStudioを開いてコードをコピーしてみると、結果は-202014162になります。それで終わりですか?明らかに違います!コンパイル時オプションをAnyCPUからx64〜に変更してみてください(サーバー環境は正確に64ビットです!!!)結果は-202014160であることが判明しました、はい、それは-202014160です。考えてみてください。浮動小数点の計算エラーがあるため、-202014160の結果は妥当です。さて、C ++をもう一度試してください。//テスト環境Intel(R)i7-3770 CPU、WindowsOS64。VisualStudio2012のデフォルト設定。


float p3x = 80838.0f;
float p2y = -2499.0f;
double v321 = p3x * p2y;
std::cout.precision(15);
std::cout << v321 << std::endl;

ええと、x86とx64はどちらも妥当な結果であるようです-202014160。おかしいです。実際、さまざまなプラットフォームでの上記のC ++コードの結果は次のとおりです。

  • Windows 32/64ビットの場合:-202014160
  • Linux 64ビット(CentOS 6 gcc 4.4.7):-202014160
  • Linux 32ビット(Ubuntu 12.04+ gcc 4.6.3)では、-202014162です。

補足:最初に、この記事は有名なプログラマーの左耳マウスであるCool Shellに提出されました。結果データのこの部分は、UncleMouseによって行われたいくつかの調整から得られたものです。(元のテキストが要点を捉えていなかったため、多く
の不満も寄せられました)妥当な計算結果は-202014160であり、正しい計算結果は-202014162です。合理性は浮動小数点の精度が不十分であることが原因です(理由は後で説明します。セックス)。2つの倍精度浮動小数点数を乗算すると、正確で妥当な結果を得ることができます。//私が使用した「正しくて合理的」という2つの単語が適切かどうかを心配する必要はありません。問題は、X64とX86の結果がC#で一貫していない理由です。

浮動小数点の計算結果は間違っていますが、合理的な説明です

80838.0f * -2499.0f = -202014160.0が妥当なのはなぜですか?

コンピューターでの32ビット浮動小数点数の表現は次のとおりです。1符号ビット(s)-8指数ビット(E)-23有効数字(M)、つまり
クロスプラットフォームによって引き起こされる浮動小数点のバグ|予期しない結果があります
、Eは実際に1.xxxxx * 2 ^に変換されます。 E、Mの指数は、1を削除した後のフロントxxxxxです(1ビットを保存)。


80838.0 如何表达?
0 = 1 0011 1011 1100 0110.0(二进制) = 1.0011 1011 1100 0110 0*2^16
有效位M = 0011 1011 1100 0110 0000 000(一共 23 位)
指数位E = 16 + 127 = 143 = 10001111

内部表示 80838.0 = 0 [10001111] [0011 1011 1100 0110 0000 000] = 0100 0111 1001 1101 1110 0011 0000 0000 = 47 9d e3 00 //实际调试时看到的内存值 可能是00 e3 9d 47是因为调试环境用了小端表示法法:低位字节排内存低地址端,高位排内存高地址

2. -2499.0 如何表达?

-2499.0 = -100111000011.0 = -1.001110000110 * 2^11
有效位M = 0011 1000 0110 0000 0000 000
指数位E = 11+127=138= 10001010
符号位s = 1
内部表示-2499.0 = 1 [10001010] [0011 1000 0110 0000 0000 000]
=1100 0101 0001 1100 0011 0000 0000 0000 =c5 1c 30 00

3. 如何计算 80838.0 * -2499.0 = ?

指数 e = 11+16 = 27
则指数位 E = e + 127 = 154 = 10011010
有效位相乘结果为 1.1000 0001 0100 1111 1011 1010 01 (可以自己动手实际算下),实际中只能有23位,后面的被截断即1000 0001 0100 1111 1011 1010 01,相乘结果内部表示=1[10011010][1000 0001 0100 1111 1011 101] = 1100 1101 0100 0000 1010 0111 1101 1101 = cd 40 a7 dd
结果 = -1.1000 0001 0100 1111 1011 101 *2^27
= -11000 0001 0100 1111 1011 1010000
= -202014160

上記のことから、32ビット浮動小数点数-202014160が妥当な結果であることがわかり、明確に説明できます。ただし、有効数字が長い場合、上記は切り捨てられません。


4. 正确的结果-202014162怎么得来?

有效位相乘结果为 1.1000 0001 0100 1111 1011 1010 01
即结果 = -1.1000 0001 0100 1111 1011 101001 *2^27
= -11000 0001 0100 1111 1011 101001 = -202014162

根本原因マイニング

上記の部分は2つの結果の出所を説明していますが、なぜ根本的な見返りがないようです。C ++、X86、X64(DEBUGの下で、これについては後で説明します)で同じコードを使用して、一貫した結果-202014160を取得します。これは、理解しやすく、合理的です。理由は何ですか?コンパイル後に生成されたコードを見てください(重要な部分をインターセプトします)


//C# x86 下
......
float p3x = 80838.0f;
0000003b mov dword ptr [ebp-40h],479DE300h
float p2y = -2499.0f;
00000042  mov dword ptr [ebp-44h],0C51C3000h
double v321 = p3x * p2y;
00000049  fld dword ptr [ebp-40h]
0000004c fmul dword ptr [ebp-44h]
0000004f  fstp qword ptr [ebp-4Ch]
.......

//C# X64下
......
float p3x = 80838.0f;
00000045  movss xmm0,dword ptr [00000098h]
0000004d  movss dword ptr [rbp+3Ch],xmm0
float p2y = -2499.0f;
00000052  movss xmm0,dword ptr [000000A0h]
0000005a movss dword ptr [rbp+38h],xmm0
double v321 = p3x * p2y;
0000005f  movss xmm0,dword ptr [rbp+38h]
00000064  mulss xmm0,dword ptr [rbp+3Ch]
00000069  cvtss2sd xmm0,xmm0
0000006d  movsd mmword ptr [rbp+30h],xmm0
......

同様のコードがC ++ x86 / x64で生成され(これがC ++ x86 / x64の結果がC#x64と一致する理由です)、浮動小数点(mulss)が乗算されてから、double(cvtss2sd)に変換されます。上記のアセンブリコードから、C#X86がコードを生成するために使用するfld / fmul / fstpなどの命令を確認できます。その中で、fld / fmul / fstpなどの命令はFPU(浮動小数点ユニット)浮動小数点演算プロセッサによって作成されます。FPUが浮動小数点演算を実行するとき、関連する浮動小数点演算に80ビットレジスタを使用します。次に、float /に従って、Doubleは32ビットまたは64ビットに切り捨てられます。非FPUの場合、SSEの128ビットレジスタが使用され(フロートは実際には32ビットのみを使用し、計算も32ビットで計算されます)、これが上記の問題の最終的な原因です。

浮動小数点演算標準IEEE-754は、標準の実装者が浮動小数点スケーラブル精度フォーマット(拡張精度)を提供することを推奨しています。Intelx86プロセッサには、この拡張をサポートするFPU(浮動小数点ユニット)浮動小数点演算プロセッサがあります。C#の浮動小数点はこの標準をサポートしており、その公式文書には、浮動小数点演算が戻り値の型よりも高精度の値を生成する可能性があること(上記の戻り値の精度がfloatの精度を超えるのと同じように)、およびハードウェアの場合も記載されていますスケーラブルな浮動小数点精度がサポートされている場合効率を向上させるために、すべての浮動小数点演算がこの精度で実行されます。たとえばx y / z、x yの値は、doubleの能力の範囲外である可能性がありますが、実際の状況おそらくzで除算した後、結果をdouble範囲に戻すことができます。この場合、FPUを使用した結果は正確なdouble値を取得し、非FPUは無限大になります。

つまり、上記の結果の理由は、非FPUの場合の2つの浮動小数点数の乗算は、32ビットの計算を使用して結果にエラーを引き起こす結果を生成するのに対し、FPUは計算に80ビットを使用するためです。その結果、精度は非常に高く、この記事の場合は1桁で2になります。したがって、コードを作成するときは、実際の実行環境/テスト環境/開発環境(OSアーキテクチャ、コンパイルオプションなどを含む)の一貫性を確保する必要があります。そうしないと、不可解な問題が発生します(この記事は、の不整合が原因です。開発環境と実行環境問題は、これが長い間苦労した後の理由であることがわかりました。浮動小数点演算に遭遇したときは、この理由が原因である可能性があることを忘れないでください。さらに、特別なことです。フロート/ダブルの混合使用に注意を払う必要があります。

要約すると、この記事では、以前に遭遇した難病を分析することにより、コンピューター内の浮動小数点数の表現を確認または調査し、疑問を解決しました。ハードウェアの底までフォローアップする必要がある場合もありますが、もちろん、ハードウェア技術の発達により、これまで当たり前だったことが、新しいハードウェアの場合とは異なる場合があります(たとえば、前述のFPU)。記事の内容もハイエンドテクノロジーに置き換えられました。ハードウェアについてはよくわかりません。興味がある場合は、より多くの資料を参照できます。より多くの参考資料については、元のテキストをお読みください)。

古いルールは、それが役立つなら(あなたの周りの他の人々に役立つ可能性があります)、記事を書くのは簡単ではありません。あなたが「それを読んで」、転送してサポートを共有できることを願っています。

おすすめ

転載: blog.51cto.com/15072927/2607565