デバッグ/リリースモードでの浮動小数点/倍精度
-
01-07-2019 - |
質問
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# などの高級言語では浮動小数点が出現した瞬間にアセンブリをデバッグする余地がまだあることも示しています。