Licencja Creative Commons

Autorem tego opracowania jest dr Piotr A. Dybczyński z Instytutu Obserwatorium Astronomiczne UAM w Poznaniu.


Co to jest "ujemne zero"?

Na początek uwaga: ujemne zero nie występuje wśród liczb całkowitych - powszechnie stosowane kodowanie uzupełnień do dwóch nie daje takiej możliwości (nie ma zresztą takiej potrzeby). Wszystko zatem co piszemy niżej dotyczy liczb zmiennoprzecinkowych.

Norma IEEE 754 definiująca standard obliczeń zmiennopozycyjnych wprowadziła dwa odmienne kodowania zera, uwzględniające różne znaki:

  • dodatnie zero - kodowane w pojedynczej precyzji jako kombinacja bitów (szesnastkowo): 0x00000000 a w podwójnej jako 0x0000000000000000.
  • ujemne zero - kodowane w pojedynczej precyzji jako kombinacja bitów (szesnastkowo): 0x80000000 a w podwójnej jako 0x8000000000000000.

Jest to sposób obejścia pewnych osobliwości rachunkowych wynikających z ograniczonych możliwości kodowania małych liczb w pamięci czy rejestrach komputera. Matematyka nie zna pojęcia ujemnego zera, co więcej ujemny znaczy z definicji mniejszy niż zero a dodatni znaczy większy niż zero , czyli z definicji zero nie może być w matematyce ani ujemne ani dodatnie. W obszarze "Computer Science" takie byty jednak jak widać istnieją i pełnią pożyteczną rolę.

Skąd się może wziąć "ujemne zero"?

Po pierwsze może pojawić się jako wynik niektórych operacji matematycznych w programie, np:

  • jeśli wynik operacji jest zbyt mały by poprawnie zakodować go jako ujemną liczbę zmiennoprzecinkową (underflow):
    • -10-250 * 10-250 da w wyniku ujemne zero,
    • również -10-250 / 10250 daje w wyniku ujemne zero.
  • Norma IEEE 754 wprowadza również dwa kodowania dla wartości -∞ oraz +∞; dzielenie liczby ujemnej przez +∞ lub dodatniej przez -∞ daje w wyniku ujemne zero:
    • 1.0/(-10250 * 10250) da w wyniku ujemne zero.
  • Jeżeli jakieś wyrażenie ma wartość ujemnego zera, to wynik jego pomożenia przez skończoną stałą dodatnią będzie nadal wynosił ujemne zero. Również wynik mnożenia dodatniego zera przez skończoną stałą ujemną będzie wynosił ujemne zero.

Po drugie od pewnego czasu istnieje możliwość wpisania ujemnego zera jako danej z klawiatury lub z pliku, np. do programu czytającego liczbę zmiennoprzecinkową ze standardowego wejścia funkcją scanf().

Po trzecie można je po prostu podstawić: x = -0.0; happy smiley

Czy "ujemne zero" to zero?

Tak! W większości przypadków pojawienie się w rachunkach ujemnego zera nie powoduje żadnych kłopotów gdyż matematycznie funkcjonuje ono jak zwykłe zero bez znaku. Wyjątkiem są operacje opisane wyżej.

Nawet jednak gdy zmiennej x nadamy rachunkiem lub na wejściu wartość ujemne zero to proste testy nie pozwolą odróżnić jej od zwykłego zera.

Warunki: (x == 0), (x == -0) oraz (x == +0) będą zawsze prawdziwe, niezależnie od tego czy zmienna x ma wartość ujemnego zera czy dodatniego zera.

W języku ANSI C, poza "ręcznym" badaniem bitu znaku w binarnej reprezentacji wartości zmiennej x mamy co najmniej trzy sposoby na ustalenie, czy zmienna x zawiera ujemne zero czy dodatnie zero:

  • skoro wynikiem dzielenia stałej dodatniej przez ujemne zero jest -∞ to warunek (x==0 && 1.0/x < 0) będzie prawdziwy tylko dla x zawierającego ujemne zero, a fałszywy dla dodatniego zera i niezerowych wartości x.
  • wprowadzone do języka C standardem C99 makro signbit() zwraca wartość niezerową gdy argument ma ustawiony bit znaku (czyli jest ujemny).
  • wprowadzona tym samym standardem ale rekomendowana już w normie IEEE-754 funkcja copysign(y,x) nadaje wartości zmiennej y znak zmiennej x. Można zatem wykonać y=1.0; copysign(y, x) a następnie warunkiem (x==0 && y<0) sprawdzić czy x zawiera ujemne zero.

Sprawdź jak Twoja implementacja języka C obsługuje "ujemne zero".

Przez implementację rozumiem tu zarówno kompilator jak i dołączone do niego standardowe biblioteki. Ważne są zarówno ich domyślne zachowania jak i możliwości ich zmiany.

Poniższy fragment kodu pokazuje jak "obsługiwane jest" ujemne zero:


#include<stdio.h>
#include<math.h>


int main(void)
{
    double x,y;
    double minus_infinity = -1.0/0.0;
    double minus_zero = 1.0/ minus_infinity ;
    union {
               double                 m_z1;
               long long unsigned int m_z2;
          }u;

    u.m_z1 = minus_zero;

    printf("ujemna nieskończoność (-inf): %f\n",minus_infinity);
    printf("ujemne zero: %f\n",minus_zero);
    printf("ujemne zero szesnastkowo: %llX\n",u.m_z2);

    /* rezultaty prostych działań */

    printf("(-0.0)*5.0 = %f\n",minus_zero*5.0);
    printf("(-0.0)*(-7.9) = %f\n",minus_zero*(-7.9));
    printf("-3.3*0.0 = %f\n",(-3.3)*0.0);
    printf("1.0/-0.0 = %f\n",1.0/minus_zero);

    if(1.0/minus_zero < 0) printf("Jest ujemne zero.\n");
    else printf("Kucha\n");

    x=-0.0;
    printf("x = %f\n",x);
    if(1.0/x < 0) printf("Jest ujemne zero.\n");
    else printf("Kucha\n");

    printf("signbit(x) = %d\n",signbit(x));
    printf("signbit(-0.0) = %d\n",signbit(-0.0));
    printf("signbit(0.0) = %d\n",signbit(0.0));

    y=3.0;
    printf("copysign(y,x)=%f\n",copysign(y,x));

    return 0;
}

Skompilowanie i wykonanie tego kodu przy pomocy kompilatora gcc (wersja 10.2.1) pod Debianem 11 dało wynik:

 ujemna nieskończoność (-inf): -inf
 ujemne zero: -0.000000
 ujemne zero szesnastkowo: 8000000000000000
 (-0.0)*5.0 = -0.000000
 (-0.0)*(-7.9) = 0.000000
 -3.3*0.0 = -0.000000
 1.0/-0.0 = -inf
 Jest ujemne zero.
 x = -0.000000
 Jest ujemne zero.
 signbit(x) = 1
 signbit(-0.0) = 1
 signbit(0.0) = 0
 copysign(y,x)=-3.000000


Źródła i uzupełnienia:


Licencja Creative Commons