Porównywanie liczb floating point

Autor: Wojciech Muła
Dodany:23.10.2002

Treść

Wprowadzenie

W procesorach Pentium nie ma rozkazu porównywania liczb zmiennoprzecinkowych, takiego który bezpośrednio modyfikował rejestr flag. Dopiero w Pentium Pro wprowadzono rozkaz fcom.

Poniżej przedstawię sposób porównywania liczb floating point tylko za pomocą rozkazów stałoprzecinkowych.

Format liczb floating point

Liczba zmiennoprzecinkowa o pojedynczej precyzji (32-bitowa) ma następującą strukturę:

31 30       23                      0
 +-+--------+-----------------------+
 |S|EXP+bias|      mantissa         |
 +-+--------+-----------------------+
  • S (1 bit) — bit znaku (dla liczb ujemnych ustawiony)
  • EXP+bias (8 bitów) — wykładnik (ang. exponent) + przesunięcie (bias)
  • mantissa (23 bity) — znormalizowana mantysa (mantissa)

Wykładnik nie jest zapisywany jako liczba ujemna, tylko liczba z przesunięciem (ang. bias) i dla liczb typu single przesunięcie wynosi 127, tak więc w NKB zawsze jest to liczba dodatnia.

Normalizacja mantysy polega na doprowadzeniu liczby zmiennoprzecinkowej do postaci 1.bity po przecinku, wobec czego część całkowita jest znana i nie zapisuje się jej.

Wartość liczby obliczana jest zgodnie ze wzorem:

− 1S ⋅ (1 + mantissa) ⋅ 2exponentbias

Ponieważ za znak odpowiada 1 bit wyznaczenie modułu lub wartości przeciwnej jest banalne:

; eax - liczba zmiennoprzecinkowa (32 bity)

and eax, 7fffffffh ; moduł
xor eax, 80000000h ; wartość przeciwna

Porównywanie

Jeśli policzyć wartość dwusłowa, zawierającego liczbę typu float, jako liczbę zapisaną w naturalnym kodzie binarnym (NKB), to jej wartość wyniesie:

(S shl 31) or ((EXP+Bias) shl 23) or mantissa

Jak widać wartościowanie pól EXP+Bias i mantissa jest takie samo jak we wzorze na wartość liczby floating-point (większą wagę ma wykładnik). Jedynie bit znaku S ma niewłaściwą (zbyt dużą) wagę — wartość w NKB liczby ujemnej będzie zawsze większa od liczby dodatniej.

Z tego powodu trzeba rozpatrzeć trzy przypadki:

  1. obie liczby są dodatnie — S=0, więc wystarczy zwykłe porównanie
  2. obie liczby są ujemne — przed porównaniem należy odwrócić kolejność liczb
  3. liczby mają różne znaki — należy porównywać liczby przeciwne (zanegowane bity znaku).

Kod w języku C

int compare_fp(float A, float B) {

 // liczby binarne
 dword a = (dword*)&A;
 dword b = (dword*)&B;

 // znaki liczb
 dword sign_a = a & 0x80000000;
 dword sign_b = b & 0x80000000;

 // obie dodatnie
 if ((sign_a == 0) && (sign_b == 0))
        return a > b;

 // obie ujemne
 if ((sign_a != 0) && (sign_b != 0))
        return b > a;

 // różne znaki
 a ^= 0x80000000;
 b ^= 0x80000000;
 return a > b;
}

Kod w asemblerze (bez rozgałęzień)

; wejście: eax - a
;          ebx - b
; wyjście: znaczniki flag ustawione zgodnie z warunkiem float(eax) & float(ebx)

compare_fp:
        mov ecx, eax
        mov edx, ebx
        and ecx, 0x80000000 ; ecx := sign_a
        and edx, 0x80000000 ; edx := sign_b

        ; jeśli obie liczby ujemne zamień ich kolejność
        ; cond1 := (float(eax) < 0 and float(ebx) < 0)
        push ecx
        push edx

        and  ecx, edx       ;
        sar  ecx, 31        ; ecx = cond1 ? 0xffffffff  : 0x00000000

        mov  edx, ebx       ;
        sub  edx, eax       ;
        and  edx, ecx       ; edx = cond1 ? ebx - eax   : 0x00000000

        add  eax, edx       ; eax = cond1 ? eax+ebx-eax : eax
        sub  ebx, edx       ; ebx = cond1 ? ebx-ebx+eax : ebx

        ; jeśli liczby są różnych znaków zaneguj bity znaku
        ; cond2 := (float(eax) < 0 xor float(ebx) < 0)
        pop  ecx
        pop  edx
        xor  ecx, edx       ; ecx = cond2 ? 0x80000000  : 0x00000000
        xor  eax, ecx
        xor  edx, ecx

        ; na końcu porównaj
        cmp  eax, ebx
        ret