Ir para conteúdo

Uma dica, semi-portável, para lidar com comparações de valores em ponto-flutuante


fredericopissarra

Posts Recomendados

Lidar com ponto flutuante, além dos problemas com exatidão, tem outro entrave: Algumas operações são bem mais lentas do que as equivalentes para tipos inteiros. Mas, infelizmente, nem sempre podemos nos livrar da aritmética em ponto flutuante. O que podemos fazer é aproveitar algumas características da estrutura dos tipos (especialmente float e double) e tentarmos tornar algumas operações mais rápidas.

Por exemplo, já citei o “macete” de retornar -1, 0 ou 1 de acordo com a comparação de dois valores inteiros:

1
2
int comp1( int a, int b )
{ return (a > b) - (a < b); }

E, é claro, isso aplica-se á ponto flutuante também:

1
2
int comp2( float a, float b )
{ return (a > b) - (a < b); }

Embora existam alguns problemas com o código acima, devido à precisão, vamos assumir que essas comparações possam ser feitas sem problemas e darmos uma olhada nos códigos gerados pelo compilador para x86-64:

1
2
3
4
5
6
7
8
9
comp2:                 comp1:        
  xor eax, eax            xor eax, eax
  ucomiss xmm0, xmm1      cmp edi, esi
  seta  al                setl  dl
  xor edx, edx            setg  al
  ucomiss xmm1, xmm0      movzx edx, dl
  seta  dl                sub eax, edx
  sub eax, edx            ret
  ret

Reparem que o código que lida com ints faz apenas uma comparação, mas o código dos floats faz duas e a instrução ucomiss é lenta, em relação à cmp. No resto, as funções são idênticas.

Agora, aqui vai o macete interessante:

A estrutura de um float nos garante, se tivermos valores sub-normais ou normalizados, que os valores inteiros correspondentes a eles (não convertido a partir de) possam ser comparados, como inteiros, sem problemas. Ou seja, essas duas comparações são “idênticas”:

1
2
3
4
5
6
7
// a e b são floats
 
// compara floats.
(a > b) - (a < b);
 
// compara ints.
(*(int *)&a > *(int *)&b) - (*(int *)&a < *(int *)&b);

É bom lembrar que um valor em ponto flutuante é uma fração onde apenas o numerador e o expoente da escala estão armazenados (bem como o sinal), ou seja:

ieee_754_single_floating_point_format.pnEstrutura de um float

Onde a fração f e o expoente da escala, e, são inteiros positivos.

Aparentemente essa comparação com float como se fossem inteiros não funciona porque a representação de valores negativos em complemento 2 não bate, não é? Acontece que a estrutura foi feita justamente para isso! A posição dos campos da estrutura permitem a comparação “como se” os valores correspondentes à estrutura fossem inteiros sinalizados! Isso é fácil de provar, mas levaria muito tempo e este texto ficaria grande. No entanto, eis um exemplo:

Considere que tenhamos dois valores: 9.0 e 10.0. Nas estruturas dos valores binários, antes do escalonamento, 9 é 0b1.001 e 10 é 0b1.01. A escala deslocará o ponto binário exatamente 3 bits para a direita, ou seja, num float, e=130. a fração, excluindo o 1 implícito terá f como 0b00100000000000000000000 e 0b01000000000000000000000, para 9 e 10, respectivamente. Os valores correspondentes às estruturas serão, para 9, s=0, e=130, f=0x100000; e para o 10 apenas fmuda para 0x200000. Ou seja, 9 é codificado como 0x41100000 e 10 como 0x41200000. Mas, e quanto a -9 e -10? Da mesma maneira, mas apenas o bit de sinal muda e teremos -9 como 0xc1100000 e -10 como 0xc1200000, onde, é claro, numa comparação sinalizada, 0xc1100000 é maior que 0xc1200000.

A mesma lógica aplica-se aos tipos double, só que eles têm 64 bits e, então, deve-se usar o tipo long long ou int64_t. Mas, o macete pode não funcionar para o tipo long double porque ele tem 10 bytes de tamanho e, embora, por alinhamento, o compilador reserve 16, possivelmente os 6 bits que faltam podem ter “lixo”… Mas, você pode usar o macete com os tipos __float128 e __int128, se seu compilador suportá-los.

Como ficamos?

Comparações entre valores em ponto flutuante podem ser feitos pelo valor inteiro, equivalente as suas estruturas. Ou seja, no exemplo das rotinas de comparação lá em cima, poderíamos fazer:

1
2
3
4
5
// compara floats.
int r = comp2(a, b) ...
 
 // compara estrutura de floats.
int r = comp1(*(int *)&a, *(int *)&b);

A segunda opção é mais rápida…

Atenção!!!

O macete funciona se os valores não forem NaNs ou Infs. Nesses casos, mesmo a comparação em ponto flutuante é projetada para “falhar”. Aliás, a instrução ucomiss, lida com comparações escalares de precisão simples (ss = single scalar) e o “u” significa “unordered”, que quer dizer que se um ou mais dos valores for NaN os flags serão ajustado de acordo (este é o único caso onde PF será 1 – e UCOMISS sempre zera SF e OF. É como se a comparação fosse não sinalizada!).

Um outro detalhe é que isso só funciona se estivermos lidando com ponto flutuante, binário, no padrão IEEE-754. Sim, existem tipos “decimais” no IEEE 754, mas a biblioteca padrão não tem um bom suporte… Existem, também, plataformas que não usam o padrão IEEE, mas são extremamente raras hoje em dia…

Fonte: https://wp.me/pudbF-1Fo

Link para o comentário
Compartilhar em outros sites

Arquivado

Este tópico foi arquivado e está fechado para novas respostas.

  • Quem Está Navegando   0 membros estão online

    • Nenhum usuário registrado visualizando esta página.
×
×
  • Criar Novo...