質問

C#/.NET の浮動小数点演算はデバッグ モードとリリース モードで精度が異なりますか?

役に立ちましたか?

解決

確かにそれらは異なる可能性があります。CLR ECMA 仕様によると:

浮動小数点数(statics、配列要素、クラスのフィールド)のストレージ場所は固定サイズです。サポートされているストレージサイズは、float32とfloat64です。他のどこでも(評価スタック、引数として、リターンタイプとして、およびローカル変数として)浮動小数点数は、内部フローティングポイントタイプを使用して表されます。このような場合、変数または式の公称タイプはR4またはR8のいずれかですが、その値は追加の範囲および/または精度で内部的に表すことができます。内部浮動小数点表現のサイズは実装依存であり、変化する可能性があり、少なくとも表現される変数または式のサイズと同じくらい優れているものとします。これらのタイプがストレージからロードされたときに、float32またはfloat64からの内部表現への暗黙の拡大変換が実行されます。内部表現は、通常、ハードウェアのネイティブサイズ、または操作の効率的な実装に必要なものです。

これが基本的に意味するのは、次の比較が等しい場合もあれば、等しくない場合もあるということです。

class Foo
{
  double _v = ...;

  void Bar()
  {
    double v = _v;

    if( v == _v )
    {
      // Code may or may not execute here.
      // _v is 64-bit.
      // v could be either 64-bit (debug) or 80-bit (release) or something else (future?).
    }
  }
}

持ち帰りメッセージ:浮動小数点値が等しいかどうかを決してチェックしないでください。

他のヒント

これは興味深い質問なので、少し実験してみました。私はこのコードを使用しました:

static void Main (string [] args)
{
  float
    a = float.MaxValue / 3.0f,
    b = a * a;

  if (a * a < b)
  {
    Console.WriteLine ("Less");
  }
  else
  {
    Console.WriteLine ("GreaterEqual");
  }
}

DevStudio 2005 と .Net 2 を使用します。デバッグとリリースの両方としてコンパイルし、コンパイラーの出力を調べました。

Release                                                    Debug

    static void Main (string [] args)                        static void Main (string [] args)
    {                                                        {
                                                        00000000  push        ebp  
                                                        00000001  mov         ebp,esp 
                                                        00000003  push        edi  
                                                        00000004  push        esi  
                                                        00000005  push        ebx  
                                                        00000006  sub         esp,3Ch 
                                                        00000009  xor         eax,eax 
                                                        0000000b  mov         dword ptr [ebp-10h],eax 
                                                        0000000e  xor         eax,eax 
                                                        00000010  mov         dword ptr [ebp-1Ch],eax 
                                                        00000013  mov         dword ptr [ebp-3Ch],ecx 
                                                        00000016  cmp         dword ptr ds:[00A2853Ch],0 
                                                        0000001d  je          00000024 
                                                        0000001f  call        793B716F 
                                                        00000024  fldz             
                                                        00000026  fstp        dword ptr [ebp-40h] 
                                                        00000029  fldz             
                                                        0000002b  fstp        dword ptr [ebp-44h] 
                                                        0000002e  xor         esi,esi 
                                                        00000030  nop              
      float                                                      float
        a = float.MaxValue / 3.0f,                                a = float.MaxValue / 3.0f,
00000000  sub         esp,0Ch                            00000031  mov         dword ptr [ebp-40h],7EAAAAAAh
00000003  mov         dword ptr [esp],ecx                
00000006  cmp         dword ptr ds:[00A2853Ch],0        
0000000d  je          00000014                            
0000000f  call        793B716F                            
00000014  fldz                                            
00000016  fstp        dword ptr [esp+4]                    
0000001a  fldz                                            
0000001c  fstp        dword ptr [esp+8]                    
00000020  mov         dword ptr [esp+4],7EAAAAAAh        
        b = a * a;                                                b = a * a;
00000028  fld         dword ptr [esp+4]                    00000038  fld         dword ptr [ebp-40h] 
0000002c  fmul        st,st(0)                            0000003b  fmul        st,st(0) 
0000002e  fstp        dword ptr [esp+8]                    0000003d  fstp        dword ptr [ebp-44h] 

      if (a * a < b)                                          if (a * a < b)
00000032  fld         dword ptr [esp+4]                    00000040  fld         dword ptr [ebp-40h] 
00000036  fmul        st,st(0)                            00000043  fmul        st,st(0) 
00000038  fld         dword ptr [esp+8]                    00000045  fld         dword ptr [ebp-44h] 
0000003c  fcomip      st,st(1)                            00000048  fcomip      st,st(1) 
0000003e  fstp        st(0)                                0000004a  fstp        st(0) 
00000040  jp          00000054                            0000004c  jp          00000052 
00000042  jbe         00000054                            0000004e  ja          00000056 
                                                        00000050  jmp         00000052 
                                                        00000052  xor         eax,eax 
                                                        00000054  jmp         0000005B 
                                                        00000056  mov         eax,1 
                                                        0000005b  test        eax,eax 
                                                        0000005d  sete        al   
                                                        00000060  movzx       eax,al 
                                                        00000063  mov         esi,eax 
                                                        00000065  test        esi,esi 
                                                        00000067  jne         0000007A 
      {                                                          {
        Console.WriteLine ("Less");                        00000069  nop              
00000044  mov         ecx,dword ptr ds:[0239307Ch]                Console.WriteLine ("Less");
0000004a  call        78678B7C                            0000006a  mov         ecx,dword ptr ds:[0239307Ch] 
0000004f  nop                                            00000070  call        78678B7C 
00000050  add         esp,0Ch                            00000075  nop              
00000053  ret                                                  }
      }                                                    00000076  nop              
      else                                                00000077  nop              
      {                                                    00000078  jmp         00000088 
        Console.WriteLine ("GreaterEqual");                      else
00000054  mov         ecx,dword ptr ds:[02393080h]              {
0000005a  call        78678B7C                            0000007a  nop              
      }                                                            Console.WriteLine ("GreaterEqual");
    }                                                    0000007b  mov         ecx,dword ptr ds:[02393080h] 
                                                        00000081  call        78678B7C 
                                                        00000086  nop              
                                                              }

上記のことからわかることは、浮動小数点コードはデバッグとリリースの両方で同じであり、コンパイラは最適化よりも一貫性を選択しているということです。プログラムは間違った結果 (a * a が b 以上) を生成しますが、これはデバッグ/リリース モードに関係なく同じです。

さて、インテル IA32 FPU には 8 つの浮動小数点レジスターがあるため、コンパイラーは最適化時にメモリーに書き込むのではなくレジスターを使用して値を保管し、その結果パフォーマンスが向上すると考えるでしょう。次のようなことになります。

fld         dword ptr [a] ; precomputed value stored in ram == float.MaxValue / 3.0f
fmul        st,st(0) ; b = a * a
; no store to ram, keep b in FPU
fld         dword ptr [a]
fmul        st,st(0)
fcomi       st,st(0) ; a*a compared to b

ただし、これはデバッグ バージョンとは異なる方法で実行されます (この場合、正しい結果が表示されます)。ただし、ビルド オプションに応じてプログラムの動作を変えるのは非常に悪いことです。

FPU コードは、コードを手作業で作成することでコンパイラーよりも大幅にパフォーマンスが向上する可能性がある領域の 1 つですが、FPU の動作方法についてよく理解しておく必要があります。

実際、デバッグ モードで x87 FPU を使用し、リリース モードで float-ops に SSE を使用する場合は、異なる可能性があります。

違いをデモンストレーションしてほしいという上記の(コメントでの)フランク・クルーガーのリクエストに応えて:

最適化なしでGCCでこのコードをコンパイルし、-mfpmath = 387(他のコンパイラでは機能しないと思う理由はありませんが、試したことはありません。)その後、最適化なしと-msse -mfpmath = SSE。

出力が異なります。

#include <stdio.h>

int main()
{
    float e = 0.000000001;
    float f[3] = {33810340466158.90625,276553805316035.1875,10413022032824338432.0};
    f[0] = pow(f[0],2-e); f[1] = pow(f[1],2+e); f[2] = pow(f[2],-2-e);
    printf("%s\n",f);
    return 0;
}

デバッグ モードとリリース モードで結果が異なるだけでなく、プラットフォームとして x86 を使用するか x84 を使用するかによって結果が異なる簡単な例を次に示します。

Single f1 = 0.00000000002f;
Single f2 = 1 / f1;
Double d = f2;
Console.WriteLine(d);

これにより、次の結果が書き込まれます。

            Debug       Release
x86   49999998976   50000000199,7901
x64   49999998976   49999998976

逆アセンブリ (Visual Studio の [デバッグ] -> [Windows] -> [逆アセンブリ]) をざっと見てみると、ここで何が起こっているかについてのヒントが得られます。x86 の場合:

Debug                                       Release
mov         dword ptr [ebp-40h],2DAFEBFFh | mov         dword ptr [ebp-4],2DAFEBFFh  
fld         dword ptr [ebp-40h]           | fld         dword ptr [ebp-4]   
fld1                                      | fld1
fdivrp      st(1),st                      | fdivrp      st(1),st
fstp        dword ptr [ebp-44h]           |
fld         dword ptr [ebp-44h]           |
fstp        qword ptr [ebp-4Ch]           |
fld         qword ptr [ebp-4Ch]           |
sub         esp,8                         | sub         esp,8 
fstp        qword ptr [esp]               | fstp        qword ptr [esp]
call        6B9783BC                      | call        6B9783BC

特に、一見冗長に見える「浮動小数点レジスタの値をメモリに格納し、すぐにメモリから浮動小数点レジスタにロードし直す」一連の処理が、リリース モードで最適化されて取り除かれていることがわかります。ただし、2 つの指示

fstp        dword ptr [ebp-44h]  
fld         dword ptr [ebp-44h]

x87 レジスタの値を +5.0000000199790138e+0010 から +4.9999998976000000e+0010 に変更するだけで十分です。これは、逆アセンブリを段階的に実行し、関連するレジスタの値を調査することで確認できます ([デバッグ] -> [Windows] -> [レジスタ] をクリックして右に進みます)。をクリックして「浮動小数点」にチェックを入れます)。

x64 の場合はまったく異なります。同じ最適化によりいくつかの命令が削除されていますが、今回はすべてが 128 ビット レジスタと専用命令セットを備えた SSE に依存しています。

Debug                                        Release
vmovss      xmm0,dword ptr [7FF7D0E104F8h] | vmovss      xmm0,dword ptr [7FF7D0E304C8h]  
vmovss      dword ptr [rbp+34h],xmm0       | vmovss      dword ptr [rbp-4],xmm0 
vmovss      xmm0,dword ptr [7FF7D0E104FCh] | vmovss      xmm0,dword ptr [7FF7D0E304CCh]
vdivss      xmm0,xmm0,dword ptr [rbp+34h]  | vdivss      xmm0,xmm0,dword ptr [rbp-4]
vmovss      dword ptr [rbp+30h],xmm0       |
vcvtss2sd   xmm0,xmm0,dword ptr [rbp+30h]  | vcvtss2sd   xmm0,xmm0,xmm0 
vmovsd      qword ptr [rbp+28h],xmm0       |
vmovsd      xmm0,qword ptr [rbp+28h]       |
call        00007FF81C9343F0               | call        00007FF81C9343F0 

ここで、SSE ユニットは内部で単精度よりも高い精度の使用を回避するため (x87 ユニットは使用します)、最適化に関係なく、x86 の場合の「単精度っぽい」結果が得られます。実際、(Visual Studio レジスタの概要で SSE レジスタを有効にした後) vdivss, XMM0 には 0000000000000000-00000000513A43B7 が含まれており、これは先ほどの 49999998976 とまったく同じです。

両方の矛盾が実際に私を悩ませました。この例では、浮動小数点の等価性を決して比較してはいけないことを示しているだけでなく、C# などの高級言語では浮動小数点が出現した瞬間にアセンブリをデバッグする余地がまだあることも示しています。

ライセンス: CC-BY-SA帰属
所属していません StackOverflow
scroll top