Float / precisão dupla na depuração modos de liberação /
-
01-07-2019 - |
Pergunta
Do C # /. Operações NET ponto flutuante diferiram em precisão entre o modo de depuração e modo de versão?
Solução
Eles podem realmente ser diferente. De acordo com a especificação CLR ECMA:
locais de armazenamento de ponto flutuante números (estática, os elementos da matriz, e campos de classes) são de tamanho fixo. Os tamanhos de armazenamento são suportados float32 e float64. Em qualquer outro lugar (Sobre a pilha de avaliação, quanto argumentos, como tipos de retorno, e como variáveis ??locais) de ponto flutuante os números são representados utilizando um interno tipo de ponto flutuante. Em cada como exemplo, o tipo nominal do variável ou expressão é qualquer um ou R4 R8, mas seu valor pode ser representado internamente com faixa adicional e / ou precisão. O tamanho do representação interna de ponto flutuante é dependente da aplicação, podem variar, e terá precisão pelo menos tão grande como o da variável ou expressão a ser representada. A conversão alargando implícita para o representação interna do float32 ou float64 é realizada quando aqueles tipos são carregados a partir do armazenamento. o representação interna é tipicamente o tamanho nativo para o hardware, ou conforme necessário para eficiente implementação de uma operação.
O que isto significa, basicamente, é que a seguinte comparação pode ou não ser igual:
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?).
}
}
}
Tome-home mensagem:. Nunca verificar valores flutuante para a igualdade
Outras dicas
Esta é uma pergunta interessante, então eu fiz um pouco de experimentação. Eu usei esse código:
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");
}
}
usando DevStudio 2005 e .Net 2. Eu compilado como ambos os de depuração e liberação e examinou a saída do compilador:
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
}
O que os shows acima é que o código de ponto flutuante é o mesmo para ambos os de depuração e liberação, o compilador é escolher consistência ao longo de otimização. Embora o programa produz o resultado errado (a * a não ser inferior a b) é o mesmo, independentemente do modo de depuração / release.
Agora, a Intel IA32 FPU tem oito registradores de ponto flutuante, você poderia pensar que o compilador iria usar os registros para armazenar valores em termos de optimização, em vez de escrever para a memória, melhorando assim o desempenho, algo ao longo das linhas de:
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
mas isso iria executar de forma diferente para a versão de depuração (neste caso, apresentar o resultado correto). No entanto, alterar o comportamento do programa, dependendo das opções de compilação é uma coisa muito ruim.
código FPU é uma área onde a mão elaborar o código pode significativamente realizar-se o compilador, mas você precisa para obter a sua cabeça em torno da forma como funciona o FPU.
Na verdade, eles podem ser diferentes se o modo de depuração usa o x87 FPU e modo de versão usa SSE para bóia-ops.
Em resposta ao pedido de Frank Krueger acima (nos comentários) para uma demonstração de uma diferença:
Compilar este código no gcc sem otimizações e -mfpmath = 387 (não tenho qualquer razão para pensar que não iria funcionar em outros compiladores, mas eu não tentei.) Em seguida, compilá-lo sem otimizações e -msse -mfpmath = sse.
A saída será diferente.
#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;
}
Aqui está um exemplo simples onde os resultados não só diferem entre depuração e modo de liberação, mas a maneira pela qual eles o fazem depender se alguém usa x86 ou x84 como plataforma:
Single f1 = 0.00000000002f;
Single f2 = 1 / f1;
Double d = f2;
Console.WriteLine(d);
Este escreve os seguintes resultados:
Debug Release
x86 49999998976 50000000199,7901
x64 49999998976 49999998976
Um rápido olhar para a desmontagem (Debug -> Windows -> Desmontagem no Visual Studio) dá algumas dicas sobre o que está acontecendo aqui. Para o caso 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
Em particular, vemos que um monte de aparentemente redundante "armazenar o valor do registrador de ponto flutuante na memória, então imediatamente carregá-lo de volta a partir da memória para o registro de ponto flutuante" foram otimizadas no modo de versão. No entanto, as duas instruções
fstp dword ptr [ebp-44h]
fld dword ptr [ebp-44h]
são suficientes para alterar o valor no registo x87 de + 5.0000000199790138e + 0010 para + 4.9999998976000000e + 0010 como se pode verificar por percorrendo a desmontagem e investigar os valores dos registros relevantes (Debug -> Windows -> Registros , em seguida, clique direito e verificar "ponto flutuante").
A história de x64 é totalmente diferente. Ainda vemos a mesma otimização removendo algumas instruções, mas desta vez, tudo depende de SSE com seus registradores de 128 bits e conjunto de instruções dedicado:
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
Aqui, porque a unidade SSE evita usar maior precisão do que precisão simples internamente (enquanto a unidade x87 faz), acabamos com o resultado "single precisão-ish" do caso x86 independentemente de otimizações. Na verdade, encontra-se (depois de ativar os registradores SSE na visão geral do Visual Studio Registros) que depois vdivss
, XMM0 contém 0000000000000000-00000000513A43B7 que é exatamente o 49999998976 de antes.
Ambos os discrepâncias me mordeu na prática. Além de ilustrar que nunca se deve comparar a igualdade de pontos flutuantes, a exemplo também mostra que ainda há espaço para a montagem de depuração em uma linguagem de alto nível, como C #, no momento em pontos flutuantes aparecer.