Se eu copiar um float para outra variável, eles serão iguais?

167

Eu sei que usar ==para verificar a igualdade de variáveis ​​de ponto flutuante não é uma boa maneira. Mas eu só quero saber isso com as seguintes declarações:

float x = ...

float y = x;

assert(y == x)

Desde que yé copiado x, a afirmação será verdadeira?

Wei Li
fonte
78
Deixe-me fornecer uma recompensa de 50 para alguém que realmente comprova a desigualdade por meio de uma demonstração com código real. Eu quero ver a coisa de 80 vs 64 bits em ação. Mais outros 50 para uma explicação do código assembler gerado que mostra uma variável em um registrador e a outra não (ou seja qual for o motivo da desigualdade, eu gostaria que fosse explicado em um nível baixo).
Thomas Weller
11
@ThomasWeller o bug do GCC sobre isso: gcc.gnu.org/bugzilla/show_bug.cgi?id=323 ; no entanto, apenas tentei reproduzi-lo em um sistema x86-64 e isso não ocorre, mesmo com o -ffast-math. Eu suspeito que você precisa de um GCC antigo em um sistema de 32 bits.
pjc50 13/01
5
@ pjc50: Na verdade, você precisa de um sistema de 80 bits para reproduzir o bug 323; é o FPU 80x87 que causou o problema. x86-64 usa o SSE FPU. Os bits extras causam o problema, porque são arredondados ao derramar um valor em uma flutuação de 32 bits.
MSalters 13/01
4
Se a teoria do MSalters estiver correta (e eu suspeito que esteja), você poderá reproduzir novamente compilando 32 bits ( -m32) ou instruindo o GCC a usar o x87 FPU ( -mfpmath=387).
Cody Gray
4
Altere "48 bits" para "80 bits" e, em seguida, você poderá remover o adjetivo "mítico", @Hot. Isso é precisamente o que estava sendo discutido imediatamente antes do seu comentário. O x87 (FPU para arquitetura x86) usa registradores de 80 bits, um formato de "precisão estendida".
Cody Gray

Respostas:

125

além da assert(NaN==NaN); caso apontado por kmdreko, você pode ter situações com x87-math, quando flutuadores de 80 bits são temporariamente armazenados na memória e, posteriormente, comparados com valores que ainda são armazenados dentro de um registro.

Possível exemplo mínimo, que falha com o gcc9.2 quando compilado com -O2 -m32:

#include <cassert>

int main(int argc, char**){
    float x = 1.f/(argc+2);
    volatile float y = x;
    assert(x==y);
}

Demonstração Godbolt: https://godbolt.org/z/X-Xt4R

O volatileprovavelmente pode ser omitido, se você conseguir criar registo pressão suficiente para tery armazenado e recarregado a partir da memória (mas confundir o suficiente compilador, não omitir a comparação todos juntos).

Consulte a referência de perguntas frequentes do GCC:

chtz
fonte
2
Parece estranho que os bits extras sejam considerados na comparação de floatprecisão padrão com precisão extra.
Nat
13
@Nat Ele é estranho; isso é um bug .
Lightness Races em órbita
13
@ThomasWeller Não, é um prêmio razoável. Embora eu gostaria que a resposta apontasse que esse é um comportamento não conforme
Lightness Races in Orbit
4
Eu posso estender essa resposta, apontando o que exatamente acontece no código do assembly, e que isso realmente viola o padrão - embora eu não me chamasse de advogado de idiomas, por isso não posso garantir que não haja um obscuro cláusula que permite explicitamente esse comportamento. Suponho que o OP estava mais interessado em complicações práticas em compiladores reais, não em compiladores completamente livres de bugs e totalmente compatíveis (que de fato não existem, eu acho).
chtz 14/01
4
Vale ressaltar que -ffloat-storeparece ser o caminho para evitar isso.
OrangeDog 14/01
116

Não será verdade se xfor NaN, pois as comparações sempreNaN são falsas (sim, até NaN == NaN). Para todos os outros casos (valores normais, valores subnormais, infinitos, zeros), essa afirmação será verdadeira.

O conselho para evitar ==flutuações se aplica aos cálculos devido ao fato de os números de ponto flutuante serem incapazes de expressar muitos resultados exatamente quando usados ​​em expressões aritméticas. A atribuição não é um cálculo e não há razão para que a atribuição produza um valor diferente do original.


A avaliação de precisão estendida deve ser um problema, se o padrão for seguido. De <cfloat>herdado de C [5.2.4.2.2.8] ( ênfase minha ):

Exceto pela atribuição e conversão (que removem toda a faixa e precisão extras) , os valores de operações com operandos flutuantes e sujeitos às conversões aritméticas usuais e de constantes flutuantes são avaliados para um formato cujo alcance e precisão podem ser maiores do que o exigido pelo tipo.

No entanto, como os comentários apontaram, alguns casos com certos compiladores, opções de construção e destinos podem tornar isso paradoxalmente falso.

kmdreko
fonte
10
O que se xé calculado em um registro na primeira linha, mantendo mais precisão do que o mínimo para a float. O y = xpode estar na memória, mantendo apenas floatprecisão. Em seguida, o teste de igualdade seria realizado com a memória em relação ao registrador, com diferentes precisões e, portanto, sem garantia.
David Schwartz
5
x+pow(b,2)==x+pow(a,3)pode diferir auto one=x+pow(b,2); auto two=y+pow(a,3); one==twoporque um pode comparar usando mais precisão do que o outro (se um / dois são valores de 64 bits em memória ram, enquanto valores intermediários são 80 bits em fpu). Então, a tarefa pode fazer alguma coisa, às vezes.
Yakk - Adam Nevraumont 13/01
22
@evg Claro! Minha resposta simplesmente segue o padrão. Todas as apostas estão desativadas se você disser ao seu compilador que não confessa, especialmente ao ativar a matemática rápida.
kmdreko 13/01
11
@ Voo Veja a citação na minha resposta. O valor do RHS é atribuído à variável no LHS. Não há justificativa legal para o valor resultante do LHS diferir do valor do RHS. Eu aprecio que vários compiladores tenham bugs nesse sentido. Mas se algo armazenado em um registro não tem nada a ver com isso.
Lightness Races em órbita
6
@ Voo: No ISO C ++, o arredondamento para a largura do tipo deve ocorrer em qualquer atribuição. Na maioria dos compiladores que têm como alvo x87, isso realmente acontece apenas quando o compilador decide derramar / recarregar. Você pode forçá-lo gcc -ffloat-storea cumprir estritamente. Mas esta questão é sobre x=y; x==y; sem fazer nada para qualquer um dos dois. Se yjá estiver arredondado para caber em um flutuador, a conversão em duplo ou longo duplo e voltar não mudará o valor. ...
Peter Cordes
34

Sim, ycertamente assumirá o valor de x:

[expr.ass]/2: Na atribuição simples (=), o objeto referido pelo operando esquerdo é modificado ([defns.access]) substituindo seu valor pelo resultado do operando direito.

Não há margem de manobra para outros valores a serem atribuídos.

(Outros já apontaram que uma comparação de equivalência ==será avaliada falsepara os valores de NaN.)

O problema usual do ponto flutuante ==é que é fácil não ter exatamente o valor que você pensa ter. Aqui, sabemos que os dois valores, sejam eles quais forem, são os mesmos.

Raças de leveza em órbita
fonte
7
@ThomasWeller Esse é um bug conhecido em uma implementação consequentemente não compatível. É bom mencionar isso!
Lightness Races em órbita
A princípio, pensei que a linguagem que leciona a distinção entre "valor" e "resultado" seria perversa, mas não é necessário que essa distinção seja sem diferença na linguagem de C2.2, 7.1.6; C3.3, 7.1.6; C4.2, 7.1.6 ou C5.3, 7.1.6 do rascunho da norma que você menciona.
Eric Towers
@EricTowers Desculpe, você pode esclarecer essas referências? Eu não estou encontrando o que você está apontando
Lightness Races in Orbit
@ LightnessRacesBY-SA3.0: C . C2.2 , C3.3 , C4.2 e C5.3 .
Eric Towers
@ EricTowers Sim, ainda não está seguindo você. Seu primeiro link vai para o índice do Apêndice C (não me diz nada). Todos os seus próximos quatro links vão para [expr]. Se eu devo ignorar os links e focar nas citações, fico com a confusão de que, por exemplo, C.5.3 parece não abordar o uso do termo "valor" ou do termo "resultado" (embora use "result" uma vez em seu contexto normal em inglês). Talvez você possa descrever mais claramente onde acha que o padrão faz uma distinção e fornecer uma única citação clara para isso acontecer. Obrigado!
Lightness Races em órbita
3

Sim, em todos os casos (desconsiderando os problemas de NaNs e x87), isso será verdade.

Se você fizer um teste memcmpneles, poderá testar a igualdade e comparar NaNs e sNaNs. Isso também exigirá que o compilador leve o endereço da variável que forçará o valor a 32 bits em floatvez de 80 bits. Isso eliminará os problemas do x87. A segunda afirmação aqui pretende falhar ao mostrar que ==não comparará os NaNs como verdadeiros:

#include <cmath>
#include <cassert>
#include <cstring>

int main(void)
{
    float x = std::nan("");
    float y = x;
    assert(!std::memcmp(&y, &x, sizeof(float)));
    assert(y == x);
    return 0;
}

Observe que se os NaNs tiverem uma representação interna diferente (por exemplo, mantissa diferente), o valor memcmpnão será verdadeiro.

SS Anne
fonte
1

Em casos usuais, seria avaliado como verdadeiro. (ou a declaração de afirmação não fará nada)

Editar :

Por "casos usuais", quero dizer, estou excluindo os cenários acima mencionados (como valores de NaN e unidades de ponto flutuante de 80x87), conforme apontado por outros usuários.

Dada a obsolescência dos chips 8087 no contexto atual, a questão é bastante isolada e, para que a questão seja aplicável no estado atual da arquitetura de ponto flutuante usada, é verdadeira para todos os casos, exceto para NaNs.

(referência sobre 8087 - https://home.deec.uc.pt/~jlobo/tc/artofasm/ch14/ch143.htm )

Parabéns ao @chtz por reproduzir um bom exemplo e ao @kmdreko por mencionar os NaNs - não os conhecia antes!

Anirban166
fonte
11
Eu pensei que era inteiramente possível xestar em um registro de ponto flutuante enquanto yestá carregado da memória. A memória pode ter menos precisão que um registro, causando falha na comparação.
David Schwartz
11
Esse pode ser um caso para um falso, eu não pensei tão longe. (já que o OP não forneceu quaisquer casos especiais, estou assumindo que não há restrições adicionais)
Anirban166
11
Eu realmente não entendo o que você está dizendo. Pelo que entendi, o OP está perguntando se a cópia de um float e o teste de igualdade são garantidos. Sua resposta parece estar dizendo "sim". Estou perguntando por que a resposta não é não.
David Schwartz
6
A edição torna esta resposta incorreta. O padrão C ++ exige que a atribuição converta o valor no tipo de destino - o excesso de precisão pode ser usado nas avaliações de expressão, mas não pode ser retido por meio da atribuição. É irrelevante se o valor é mantido em um registro ou memória; o padrão C ++ exige que, conforme o código seja escrito, um floatvalor sem precisão extra.
Eric Postpischil 13/01
2
@ AProgrammer Dado que teoricamente um compilador (extremamente extremamente) de bugs pode causar int a=1; int b=a; assert( a==b );uma afirmação, acho que faz sentido responder a essa pergunta em relação a um compilador que funcione corretamente (embora note que algumas versões de alguns compiladores possuem / têm conhecido por errar). Em termos práticos, se por algum motivo um compilador não remover a precisão extra do resultado de uma atribuição armazenada em um registro, deve fazê-lo antes de usar esse valor.
TripeHound 13/01
-1

Sim, ele retornará True sempre, exceto se for NaN . Se o valor da variável for NaN , ele sempre retornará False !

Valentin Popescu
fonte