Flottant / double précision en mode débogage / libération
-
01-07-2019 - |
Question
Les opérations en virgule flottante C # / .NET diffèrent-elles en précision entre les modes débogage et relâchement?
La solution
Ils peuvent en effet être différents. Selon la spécification CLR ECMA:
Emplacements de stockage pour virgule flottante nombres (statique, éléments de tableau et champs de classes) sont de taille fixe. Les tailles de stockage prises en charge sont float32 et float64. Partout ailleurs (sur la pile d'évaluation, comme arguments, comme types de retour, et comme variables locales) virgule flottante les nombres sont représentés par un type interne à virgule flottante. Dans chaque Dans ce cas, le type nominal de variable ou expression est soit R4 ou R8, mais sa valeur peut être représentée en interne avec une portée supplémentaire et / ou précision. La taille de la représentation interne en virgule flottante dépend de la mise en œuvre, peut varier, et doit avoir au moins la précision grand comme celui de la variable ou expression étant représentée. Un élargissement implicite de la conversion à la représentation interne de float32 ou float64 est effectuée lorsque ceux les types sont chargés depuis le stockage. le la représentation interne est généralement la taille native du matériel, ou au besoin pour efficace mise en œuvre d'une opération.
En gros, cela signifie que la comparaison suivante peut être différente ou non:
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?).
}
}
}
Message à retenir: ne jamais vérifier l’égalité des valeurs flottantes.
Autres conseils
C'est une question intéressante, j'ai donc fait un peu d'expérimentation. J'ai utilisé ce code:
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");
}
}
utilisant DevStudio 2005 et .Net 2. J'ai compilé en tant que débogage et version et examiné la sortie du compilateur:
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
}
Ce qui précède montre que le code en virgule flottante est identique pour le débogage et la publication. Le compilateur choisit la cohérence plutôt que l’optimisation. Bien que le programme produise un résultat erroné (a * a n'est pas inférieur à b), il est identique quel que soit le mode de débogage / libération.
Désormais, la FPU Intel IA32 dispose de huit registres à virgule flottante. On pourrait penser que le compilateur les utiliserait pour stocker les valeurs lors de l'optimisation plutôt que pour l'écriture en mémoire, améliorant ainsi les performances, comme suit:
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
mais cela fonctionnerait différemment de la version de débogage (dans ce cas, afficher le résultat correct). Cependant, changer le comportement du programme en fonction des options de compilation est une très mauvaise chose.
Le code FPU est un domaine dans lequel le code peut être considérablement amélioré par rapport aux performances du compilateur, mais vous devez comprendre le fonctionnement du FPU.
En fait, ils peuvent différer si le mode débogage utilise le FPU x87 et si le mode de publication utilise SSE pour les opérations flottantes.
En réponse à la demande de Frank Krueger ci-dessus (dans les commentaires) visant à démontrer une différence:
Compilez ce code dans gcc sans optimisations et -mfpmath = 387 (je n'ai aucune raison de penser que cela ne fonctionnerait pas sur d'autres compilateurs, mais je ne l'ai pas essayé.) Ensuite, compilez-le sans optimisation et -msse -mfpmath = sse.
La sortie sera différente.
#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;
}
Voici un exemple simple où les résultats diffèrent non seulement entre le mode débogage et le mode de publication, mais la manière dont ils le font dépend de l’utilisation de la plate-forme x86 ou x84:
Single f1 = 0.00000000002f;
Single f2 = 1 / f1;
Double d = f2;
Console.WriteLine(d);
Ceci écrit les résultats suivants:
Debug Release
x86 49999998976 50000000199,7901
x64 49999998976 49999998976
Un rapide aperçu du désassemblage (Débogage - > Windows - > Désassemblage dans Visual Studio) donne quelques indications sur ce qui se passe ici. Pour le cas 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
En particulier, nous voyons qu'un groupe apparemment "redondant" stocke en mémoire la valeur du registre à virgule flottante, puis la recharge immédiatement dans le registre à virgule flottante " ont été optimisés en mode release. Cependant, les deux instructions
fstp dword ptr [ebp-44h]
fld dword ptr [ebp-44h]
suffisent pour changer la valeur dans le registre x87 de + 5.0000000199790138e + 0010 à + 4.9999998976000000e + 0010 comme on peut le vérifier en effectuant le démontage et en recherchant les valeurs des registres correspondants (Debug - > Windows - > ; Registres, puis faites un clic droit et cochez la case "Virgule flottante").
L’histoire de x64 est extrêmement différente. Nous continuons de voir que la même optimisation supprime quelques instructions, mais cette fois-ci, tout repose sur SSE avec ses registres 128 bits et son jeu d’instructions dédiées:
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
Ici, comme l'unité SSE évite d'utiliser une précision plus élevée qu'une seule précision en interne (contrairement à l'unité x87), nous nous retrouvons avec le paramètre "simple précision". résultat de la casse x86 indépendamment des optimisations. En effet, on constate (après avoir activé les registres SSE dans la vue d'ensemble des registres de Visual Studio) qu'après X vdivss
, XMM0 contient 0000000000000000-00000000513A43 qui est exactement le 49999998976 précédent.
Les deux divergences m'ont mordu dans la pratique. En plus d’illustrer qu’il ne faut jamais comparer l’égalité des points flottants, l’exemple montre également qu’il reste encore de la place pour le débogage d’assemblage dans un langage de haut niveau tel que C #, le moment où les points flottants apparaissent.