Jak Number w JavaScripcie i float w Pythonie przechowują liczby

Większość języków statycznie typowanych, jak Java czy C, ma różne typy danych dla liczb. Na przykład jeśli musisz przechować liczbę całkowitą z zakresu [-128;127], możesz użyć byte w Javie i char w C — oba zajmują tylko 1 bajt. Jeśli musisz przechować większą całkowitą, możesz użyć typów int lub long, które zajmują odpowiednio 4 i 8 bajtów. Są też oddzielne typy do przechowywania liczb z częścią ułamkową — float, który zajmuje 4 bajty, i double z 8 bajtami. Zwykle nazywa się je formatem zmiennoprzecinkowym, a dalej zobaczymy, skąd wzięła się ta nazwa.

JavaScript i Python nie idą tym schematem. JavaScript przez długi czas miał tylko jeden ogólny typ Number, który obsługiwał każdą wartość liczbową — całkowite i ułamkowe na równi — a BigInt doszedł później jako oddzielny typ całkowitych o dowolnej precyzji. Odpowiednikami w Pythonie są int dla całkowitych o dowolnej precyzji i float dla wartości ułamkowych.

Być może widziałeś już to dziwne zachowanie — wpisz 0.1 + 0.2 do konsoli JavaScriptu i w odpowiedzi dostaniesz 0.30000000000000004, a nie 0.3. Na ten temat istnieje niezliczona liczba wątków na StackOverflow, zgłoszeń na GitHubie i wpisów blogowych, i niemal wszystkie zrzucają winę na JavaScript. Ale ta sama odpowiedź wraca w Pythonie, Javie, C i każdym innym języku, który używa IEEE-754 binary64 jako typu zmiennoprzecinkowego — 64-bitowego formatu podwójnej precyzji, leżącego pod zarówno Number JavaScriptu, jak i float Pythona.

Ta sama mechanika pod spodem przejawia się w innych zachowaniach, które wyglądają jak błędy języka, ale nimi nie są — dwie duże, ale nie aż tak duże liczby całkowite okazują się równe, albo wartość nie jest równa sama sobie:

> 9007199254740992 === 9007199254740993
true
> NaN === NaN
false

Te same wyrażenia w Pythonie — te same odpowiedzi, co do bitu:

>>> 9007199254740992.0 == 9007199254740993.0
True
>>> float('nan') == float('nan')
False

W tym artykule przejdziemy przez to, jak te 64 bity naprawdę działają według IEEE-754, przeprowadzimy te same eksperymenty w obu językach, by zobaczyć, że dają identyczne układy bitów, i ustalimy, które zachowania pochodzą od floata, a które od języka owiniętego wokół niego.

Jakie typy liczbowe udostępnia każdy z języków

Przed otwarciem bitów warto rozłożyć to, co każdy język daje na powierzchni. Oba mają oddzielne typy dla liczb całkowitych i ułamkowych, o różnych nazwach i różnych domyślnych ustawieniach:

JęzykRodzajTypSzczegóły
PythonCałkowita (bez części dziesiętnej, dokładna)intDowolna precyzja. CPython powiększa magazyn w miarę potrzeby, więc 2**1000 po prostu działa, a wynik jest dokładny.
Zmiennoprzecinkowa (64-bitowy double IEEE-754)floatTen sam format, którego rozbieraniu poświęcona jest reszta artykułu. Literał 0.5 w skrypcie Pythona to 8 bajtów.
JavaScriptCałkowita (bez części dziesiętnej, dokładna)BigIntLiczba całkowita o dowolnej precyzji, dodana później jako oddzielny typ. Zapisywana z sufiksem n (np. 5n). Najbliższy odpowiednik pythonowego int.
Zmiennoprzecinkowa (64-bitowy double IEEE-754)NumberTen sam format co float w Pythonie. JavaScript używa go i dla całkowitych, i dla ułamkowych; domyślnie nie ma oddzielnego typu całkowitego.

Każdy z języków domyślnie wybiera też inny typ, gdy napiszesz literał liczbowy. Python domyślnie bierze int i przechodzi na float tylko wtedy, gdy w literale pojawi się kropka dziesiętna:

>>> type(5)
<class 'int'>
>>> type(5.2)
<class 'float'>
>>> type(5 + 0.0)     # int + float promuje się do float
<class 'float'>

JavaScript domyślnie robi odwrotnie — każdy literał liczbowy jest typu Number (czyli zmiennoprzecinkowego), o ile nie oznaczysz go n, by zrobić z niego BigInt. typeof raportuje 'number' zarówno dla całkowitych, jak i ułamkowych:

> typeof 5
'number'
> typeof 1.5
'number'
> typeof 5n
'bigint'

A gdy już masz BigInt, nie możesz mieszać go z Number w arytmetyce — silnik odmawia, zamiast po cichu konwertować:

> 5n + 1
TypeError: Cannot mix BigInt and other types, use explicit conversions

> 5n + BigInt(1)
6n
> Number(5n) + 1     // odwrotny kierunek też działa, kosztem precyzji
6

Podział na dokładne całkowite i domyślne zmiennoprzecinkowe jest w obu językach z grubsza symetryczny, ale domyślne ustawienia są odwrócone: w Pythonie musisz świadomie wybrać float, pisząc kropkę dziesiętną; w JavaScripcie musisz świadomie zrezygnować z floata, pisząc n dla BigInt. Wszystko, o czym będziemy mówić dalej, żyje po stronie floata — float w Pythonie, Number w JavaScripcie i ten sam format IEEE-754 binary64 pod nimi obydwoma.

Reprezentowanie liczb w notacji naukowej

Zanim zaczniemy mówić o zmiennym przecinku i standardzie IEEE754, musimy najpierw przyjrzeć się temu, co znaczy przedstawić liczbę w notacji naukowej. Prawdopodobnie widziałeś wartości jak 6.022×10236.022 \times 10^{23} (liczba Avogadra) albo 2.5e-72.5\text{e-}7 (postać notacji e, często używana w kodzie) — zwykle reprezentują one wielkości zbyt duże lub zbyt małe, by wypisywać je cyfra po cyfrze, i dokładnie ten problem rozwiązuje notacja naukowa.

W postaci ogólnej liczbę w notacji naukowej można przedstawić tak:

mantysa×podstawawykładnik\text{mantysa} \times \text{podstawa}^{\text{wykładnik}}

Mantysa (significand) pokazuje cyfry znaczące liczby — nazywa się ją też często significand albo precyzją. Zera nie są uważane za znaczące; one tylko trzymają miejsce. Podstawa określa bazę systemu liczbowego, czyli 10 dla systemu dziesiętnego i 2 dla dwójkowego. Wykładnik definiuje, o ile miejsc trzeba przesunąć przecinek w lewo lub w prawo, by otrzymać pierwotną liczbę.

Każdą liczbę można przedstawić w notacji naukowej. Na przykład liczbę 7 w systemie dziesiętnym i dwójkowym można przedstawić tak:

710=71001112=11120\begin{aligned} 7_{10} &= 7 \cdot 10^{0} \\ 111_{2} &= 111 \cdot 2^{0} \end{aligned}

Wykładnik 0 po prostu pokazuje nam, że żadne dodatkowe operacje nie są potrzebne, by otrzymać pierwotną liczbę. Spójrzmy na inny przykład — liczbę 0.00000022. Cyframi znaczącymi są tu 22, więc usuńmy zera:

0.00000022=0.00000022100=0.000000221001=0.00000022100(108108)=(0.00000022108)(100108)=22108\begin{aligned} 0.00000022 &= 0.00000022 \cdot 10^{0} \\ &= 0.00000022 \cdot 10^{0} \cdot 1 \\ &= 0.00000022 \cdot 10^{0} \cdot (10^{-8} \cdot 10^{8}) \\ &= (0.00000022 \cdot 10^{8}) \cdot (10^{0} \cdot 10^{-8}) \\ &= 22 \cdot 10^{-8} \end{aligned}

Linia z 1\cdot\, 1 w środku wygląda jak operacja jałowa — i o to właśnie chodzi, to zapełniacz. Już w następnej linii zastępujemy 11 równoważnym wyrażeniem 10810810^{-8} \cdot 10^{8}, które nadal jest 11, ale w postaci, którą da się rozdzielić. Następnie 10810^{8} wchłania osiem przesunięć wiodących zer w 0.000000220.00000022 (zamieniając ją w całkowitą 2222), a pozostałe 10810^{-8} staje się nowym wykładnikiem. Te dwa czynniki są wzajemnie odwrotne, więc wartość całego wyrażenia się nie zmienia — po prostu przestawiliśmy je w potrzebną nam postać.

Powyższe obliczenie pokazuje, dlaczego wykładnik podstawy maleje, gdy przecinek przesuwa się w prawo. Wykonując mnożenie, doprowadziliśmy więc naszą pierwotną liczbę do postaci z samymi cyframi znaczącymi:

0.00000022  =  22mantysa×10podstawa8wykładnik0.00000022 \;=\; \underbrace{22}_{\text{mantysa}} \times \underbrace{10}_{\text{podstawa}}{}^{\,\overbrace{-8}^{\text{wykładnik}}}

Ponieważ użyliśmy mnożenia przez 8, musieliśmy skompensować to dzieleniem — i stąd bierze się ujemny wykładnik 8. Ten sam proces, tylko tym razem z dzieleniem, by uzyskać cyfry znaczące, można wykonać na liczbie 22300000:

22,300,000=22310522{,}300{,}000 = 223 \cdot 10^{5}

Tym razem przecinek przesunął się w lewo, więc wykładnik wzrósł. Jak widzisz, notacja naukowa to sposób na łatwą pracę z bardzo dużymi lub bardzo małymi liczbami. W zależności od wykładnika mantysa może reprezentować liczbę całkowitą albo liczbę z częścią ułamkową. Przy powrocie do pierwotnej liczby ujemny wykładnik wymaga przesunięcia przecinka w lewo. Dodatni wykładnik wymaga przesunięcia w prawo i zwykle oznacza duże liczby całkowite.

Ważne jest też zrozumienie, czym jest postać znormalizowana liczby. Liczba jest znormalizowana, gdy zapisana jest w notacji naukowej z jedną niezerową cyfrą dziesiętną przed przecinkiem. Jeśli więc weźmiemy nasze pierwotne liczby i przedstawimy je w postaci znormalizowanej, będą miały następujące reprezentacje:

0.00000022=2.210722,300,000=2.23107\begin{aligned} 0.00000022 &= 2.2 \cdot 10^{-7} \\ 22{,}300{,}000 &= 2.23 \cdot 10^{7} \end{aligned}

Przedstawienie liczb w postaci znormalizowanej umożliwia łatwe porównywanie ich rzędem wielkości.

Jak się zapewne domyślasz, liczby dwójkowe będą zawsze miały przed przecinkiem 1. Ponieważ system dwójkowy ma tylko dwie cyfry — 0 i 1 — a normalizacja wymaga, by cyfra wiodąca była niezerowa, cyfra przed przecinkiem w znormalizowanej postaci dwójkowej to zawsze 1.

Notację naukową można traktować jako zmiennoprzecinkową reprezentację liczby. Termin „zmienny przecinek” odnosi się do tego, że przecinek liczby może „płynąć” — można go umieścić gdziekolwiek względem cyfr znaczących liczby. A jak już wiemy, pierwotne położenie wskazuje wykładnik.

Zmienny przecinek według IEEE754

Standard IEEE dla arytmetyki zmiennoprzecinkowej (IEEE 754) definiuje wiele rzeczy związanych z arytmetyką zmiennoprzecinkową, ale do celów naszej eksploracji interesuje nas tylko to, jak liczby są przechowywane, zaokrąglane i dodawane. Napisałem bardzo szczegółowy artykuł wyjaśniający, jak zaokrąglać liczby dwójkowe. Zaokrąglanie jest częstą operacją i zachodzi wtedy, gdy wybrany format nie daje dość bitów, by przechować liczbę. To ważny temat, więc dobrze opanuj jego mechanikę. A teraz przyjrzyjmy się, jak przechowywane są liczby. Wszystkie dalsze przykłady będą dotyczyć głównie liczb w systemie dwójkowym.

Jak liczby są przechowywane

Standard definiuje dwa formaty używane najczęściej — pojedynczej i podwójnej precyzji. Różnią się liczbą zajmowanych bitów, a w konsekwencji zakresem liczb, jaki każdy format może przechować. Podejście do przełożenia liczby z notacji naukowej na postać IEEE754 jest takie samo dla wszystkich formatów, różni się tylko liczba bitów przydzielonych na mantysę (cyfry znaczące) i wykładnik.

Zmienny przecinek IEEE754 przydziela bity na przechowanie znaku liczby, jej mantysy (cyfr znaczących) i wykładnika. Oto jak rozkłada te bity w formacie podwójnej precyzji (64 bity na liczbę), używanym zarówno przez Number JavaScriptu, jak i float Pythona:

 63 62        52 51                                                 0    ← pozycja bitu
 0  00000000000  0000000000000000000000000000000000000000000000000000
 │       │                                  │
sign(1)  exponent(11)           mantissa(52) (significand)

Bit znaku dostaje 1 bit, wykładnik — 11 bitów, a 52 bity przydzielone są mantysie (significand). Oto tabela pokazująca liczbę bitów przydzielonych w każdym formacie:

NazwaBitów łącznieWykładnikMantysa
Pojedyncza precyzja32823
Podwójna precyzja641152

Wykładnik przechowywany jest w formacie kodu z przesunięciem. Napisałem szczegółowy artykuł wyjaśniający ten format i jego różnice względem kodu uzupełnień do dwóch. Poświęć proszę trochę czasu na zrozumienie tego tematu, bo będę z niego korzystać przy przekładaniu liczb na format zmiennoprzecinkowy.

Przykłady przechowywania liczb całkowitych

Przed rozbiorem konkretnych przypadków bit po bicie, oto konkretny obraz od początku do końca dla liczby całkowitej 7 (111 dwójkowo) — pierwotne bity, znormalizowana postać naukowa i końcowy 64-bitowy układ IEEE-754:

111  →  1.11 × 2^2  →  0  10000000001  11 0...0
^^^     ^ ^^     ^     ^  ^^^^^^^^^^^  ^^ ^^^^^
│       │ │      │     │  │            │  └── 50 zer wypełniających (mantysa ma łącznie 52 bity)
│       │ │      │     │  │            └── przechowana mantysa: „11" (bity po wiodącej 1)
│       │ │      │     │  └── przechowany wykładnik: 2 + 1023 (przesunięcie) = 1025 = 10000000001
│       │ │      │     └── bit znaku: 0 (dodatnia)
│       │ │      └── wykładnik: o ile przesunąć przecinek z powrotem
│       │ └── mantysa (bity po niejawnej wiodącej 1)
│       └── niejawna wiodąca 1 (nieprzechowywana)
└── pierwotne bity liczby całkowitej

Przejdźmy teraz krok po kroku i zobaczmy, jak według tego samego schematu przechowywane są liczby całkowite 1 i 3. Liczba 1 we wszystkich systemach liczbowych zapisywana jest jako 1, więc żadna konwersja nie jest potrzebna. W postaci naukowej można ją zapisać jako:

1.0201.0 \cdot 2^{0}

Mamy tu mantysę 1 i wykładnik 0. Na tej podstawie możesz założyć, że liczba jest reprezentowana zmiennoprzecinkowo tak:

 63 62        52 51                                                 0    ← pozycja bitu
 0  00000000000  0000000000000000000000000000000000000000000000000001
 │       │                                  │
sign  exponent                  mantissa (significand)

Sprawdźmy, czy tak naprawdę jest. Niestety ani JavaScript, ani Python nie ma wbudowanej funkcji do wypisywania surowych bitów przechowanego floata. Ale łatwo taką napisać w każdym z tych języków. Oto pomocnicza funkcja w JavaScripcie, która sama radzi sobie z kolejnością bajtów Twojego komputera — zapisuje float do Float64Array, patrzy na leżącą pod spodem pamięć jako na bajty i przechodzi je od tyłu, składając łańcuch bitów:

function to64bitFloat(number) {
    var f = new Float64Array(1);
    f[0] = number;
    var view = new Uint8Array(f.buffer);
    var i, result = "";
    for (i = view.length - 1; i >= 0; i--) {
        var bits = view[i].toString(2);
        if (bits.length < 8) {
            bits = new Array(8 - bits.length).fill('0').join("") + bits;
        }
        result += bits;
    }
    return result;
}

Odpowiednik w Pythonie to jedna linia z modułem struct — spakuj float jako 8 bajtów big-endian (>d), potem rozpakuj te same bajty jako 64-bitową liczbę całkowitą bez znaku (>Q) i sformatuj tę liczbę jako 64-znakowy łańcuch dwójkowy:

import struct

def to64bit_float(x):
    return f"{struct.unpack('>Q', struct.pack('>d', x))[0]:064b}"

Obie dają ten sam 64-znakowy łańcuch dwójkowy dla tego samego wejścia — to64bitFloat(1) w JavaScripcie i to64bit_float(1.0) w Pythonie dają identyczne bity. Dalej będziemy używać ich wymiennie.

Zatem korzystając z którejkolwiek z nich, można zobaczyć, że liczba 1 przechowywana jest tak:

 63 62        52 51                                                 0    ← pozycja bitu
 0  01111111111  0000000000000000000000000000000000000000000000000000
 │       │                                  │
sign  exponent                  mantissa (significand)

To całkowicie odbiega od założeń powyżej. W mantysie nie mamy żadnych cyfr, a w wykładniku pojawiły się jedynki. Zobaczmy teraz, dlaczego tak jest.

Kluczowy wgląd polega na tym, że IEEE-754 nie przechowuje liczby wprost — najpierw przekształca ją w znormalizowaną postać naukową (jedna niezerowa cyfra przed przecinkiem, reszta po), a potem przechowuje te części. A jak ustaliliśmy wcześniej, cyfra wiodąca przed przecinkiem w znormalizowanej postaci dwójkowej to zawsze 1więc format nie zawraca sobie głowy jej przechowywaniem. To sztuczka ukrytego bitu: niejawna 1 jest dopisywana z powrotem przez sprzęt za każdym razem, gdy wartość jest czytana, co daje nam dodatkowy bit precyzji praktycznie za darmo.

Konkretnie dla liczby 1 postacią znormalizowaną jest 1.0 × 2^0 — po przecinku nie ma żadnych cyfr, a cyfra przed nim (niejawna 1) nie jest przechowywana. Mantysie nie zostaje więc nic do zapisania, dlatego jest w całości z zer.

Zobaczmy teraz, skąd w wykładniku wzięły się jedynki. Wspomniałem wcześniej, że wykładnik przechowywany jest w kodzie z przesunięciem. Jeśli obliczymy przesunięcie:

K=2n11=102310=011111111112K = 2^{n-1} - 1 = 1023_{10} = 01111111111_{2}

widzimy, że to dokładnie to, co mamy w reprezentacji. Czyli w kodzie z przesunięciem wartość tam przechowana to w rzeczywistości 0. Jeśli nie jest jasne, jak przesunięcie daje nam 0, przeczytaj mój artykuł o kodzie z przesunięciem.

Skorzystajmy z tego, czego się wyżej dowiedzieliśmy, i spróbujmy przedstawić w postaci zmiennoprzecinkowej liczbę 3. Dwójkowo zapisywana jest jako 11. Jeśli nie pamiętasz dlaczego, sprawdź mój bardzo szczegółowy artykuł o algorytmach konwersji dziesiętno-dwójkowej. A po normalizacji liczba 3 ma taką postać (liczby dwójkowo):

11100=1.110111 \cdot 10^{0} = 1.1 \cdot 10^{1}

Po przecinku mamy tylko jedną cyfrę 1, która będzie przechowana w mantysie. Jak wyjaśniono wyżej, pierwsza cyfra przed przecinkiem nie jest przechowywana. Normalizacja dała nam też wykładnik 1. Obliczmy, jak jest on reprezentowany w kodzie z przesunięciem, i wtedy będziemy mieć wszystkie potrzebne informacje:

K=2n11=10231+1023=1024102410=100000000002\begin{aligned} K &= 2^{n-1} - 1 = 1023 \\ 1 + 1023 &= 1024 \\ 1024_{10} &= 10000000000_{2} \end{aligned}

Jedna rzecz do zapamiętania o mantysie: cyfry przechowywane są dokładnie w tej kolejności, w jakiej stoją w postaci naukowej — od lewej do prawej od przecinka. Mając to na uwadze, złóżmy wszystkie liczby w reprezentację zmiennoprzecinkową:

 63 62        52 51                                                 0    ← pozycja bitu
 0  10000000000  1000000000000000000000000000000000000000000000000000
 │       │                                  │
sign  exponent                  mantissa (significand)

Jeśli uruchomisz którąkolwiek z powyższych funkcji — to64bitFloat(3) w JavaScripcie albo to64bit_float(3.0) w Pythonie — zobaczysz, że uzyskaliśmy poprawną reprezentację.

Uwaga o kolejności bitów

Być może zauważyłeś, że bity mantysy są wyrównane do lewej — dla 3 dostaliśmy 1000…000, a dla 7 (który przerabialiśmy wcześniej) dostalibyśmy 1100…000. Może to wydawać się błędne, bo te same liczby przechowane jako zwykłe 8-bitowe całkowite to 00000011 i 00000111 — jedynki po prawej. Te dwa formaty nie są odbiciem lustrzanym; po prostu przywiązują bity do różnych punktów odniesienia.

  • Przechowywanie całkowitych: pozycje bitów reprezentują potęgi 20,21,22,2^{0}, 2^{1}, 2^{2}, \ldots rosnące od prawej. Bity liczb całkowitych skupiają się po prawej, bo tam mieszkają najmniejsze wielkości (20=12^{0} = 1).
  • Mantysa zmiennoprzecinkowa: bity stoją po przecinku w postaci znormalizowanej i reprezentują potęgi 21,22,23,2^{-1}, 2^{-2}, 2^{-3}, \ldots malejące od przecinka. Skrajnie lewy przechowany bit to 212^{-1}, następny 222^{-2} i tak dalej. Zatem mantysa dla 1.1121.11_{2} przechowuje 11 od lewej do prawej, kładąc pierwszą 1 na 212^{-1}, a drugą na 222^{-2} — wyrównanie do lewej, bo tam mieszkają największe wielkości ułamkowe.

Analogia dziesiętna pokazuje to samo: całkowita 123 jest wyrównana do prawej (3 to rząd jedności). Ułamek 0.123 jest wyrównany do lewej (1 to rząd części dziesiątych). Oba nadal traktują lewą stronę jako najbardziej znaczącą — po prostu mierzą od różnych punktów odniesienia (prawa krawędź kontra przecinek).

Dlaczego 0.1+0.2 nie równa się 0.3

Teraz, gdy wiemy, jak przechowywane są liczby, zobaczmy, co dzieje się w tym często cytowanym przykładzie. Krótkie wyjaśnienie sprowadza się do tego, które ułamki da się w ogóle dokładnie przedstawić dwójkowo.

Tylko ułamki o mianowniku będącym potęgą dwójki dają się skończenie przedstawić w postaci dwójkowej. Ponieważ mianowniki 0.1 (1/10) i 0.2 (1/5) nie są potęgami dwójki, tych liczb nie da się skończenie przedstawić w formacie dwójkowym. Aby przechować je jako liczbę zmiennoprzecinkową IEEE-754, trzeba je zaokrąglić do dostępnej liczby bitów mantysy — 10 bitów dla połowicznej precyzji, 23 bity dla pojedynczej lub 52 bity dla podwójnej. W zależności od tego, ile bitów precyzji jest dostępnych, zmiennoprzecinkowe przybliżenia 0.1 i 0.2 mogą być nieco mniejsze lub większe od odpowiadających im reprezentacji dziesiętnych, ale nigdy równe. Właśnie dlatego nigdy nie dostaniesz 0.1 + 0.2 == 0.3.

Niektórym programistom takie wyjaśnienie może wystarczyć, ale najlepszym sposobem, by zobaczyć, co dzieje się pod spodem, jest samemu wykonać wszystkie obliczenia, które robi komputer. Właśnie to teraz zrobię.

Reprezentowanie 0.1 i 0.2 w formacie zmiennoprzecinkowym

Spójrzmy na układ bitów 0.1 w postaci zmiennoprzecinkowej. Pierwszą rzeczą, którą musimy zrobić, jest przekonwertowanie 0.1 na postać dwójkową. Da się to zrobić algorytmem mnożenia przez 2. Jego mechanikę wyjaśniam w artykule o algorytmach konwersji dziesiętno-dwójkowej. Jeśli przekonwertujemy 0.1 na postać dwójkową, dostaniemy ułamek nieskończony:

0.1 · 2 = 0.2     0.0...
0.2 · 2 = 0.4     0.00...
0.4 · 2 = 0.8     0.000...
0.8 · 2 = 1.6     0.0001...
0.6 · 2 = 1.2     0.00011...
0.2 · 2 = 0.4     0.000110...
0.110=0.00011001100112=0.000110.1_{10} = 0.0001100110011\ldots_{2} = 0.0\overline{0011}

Następnym krokiem jest przedstawienie tej liczby w znormalizowanej notacji naukowej:

0.110=0.00011220=1.100112240.1_{10} = 0.0\overline{0011}_{2} \cdot 2^{0} = 1.1\overline{0011}_{2} \cdot 2^{-4}

Ponieważ mantysa może mieć tylko 52 bity, musimy zaokrąglić naszą nieskończoną liczbę do 52 bitów po przecinku.

1.1001124  =  1.100110011001100152 bity10011241.1\overline{0011} \cdot 2^{-4} \;=\; 1.\underbrace{1001\,1001\,\ldots\,1001\,1001}_{52\text{ bity}}\,1\overline{0011}\ldots \cdot 2^{-4}

Zgodnie z regułami zaokrąglania zdefiniowanymi przez standard IEEE-754 i wyjaśnionymi w moim artykule o zaokrąglaniu liczb dwójkowych musimy zaokrąglić liczbę w górę do:

1.100110011001101052 bity241.\underbrace{1001\,1001\,\ldots\,1001\,1010}_{52\text{ bity}} \cdot 2^{-4}

Ostatnią rzeczą jest obliczenie reprezentacji wykładnika w kodzie z przesunięciem:

K=21111=10234+1023=101910101910=011111110112\begin{aligned} K &= 2^{11-1} - 1 = 1023 \\ -4 + 1023 &= 1019_{10} \\ 1019_{10} &= 01111111011_{2} \end{aligned}

A umieszczona w reprezentacji zmiennoprzecinkowej liczba 0.1 ma następujący układ bitów:

 63 62        52 51                                                 0    ← pozycja bitu
 0  01111111011  1001100110011001100110011001100110011001100110011010
 │       │                                  │
sign  exponent                  mantissa (significand)

Zachęcam Cię do samodzielnego obliczenia zmiennoprzecinkowej reprezentacji 0.2. Powinieneś skończyć z następującymi reprezentacjami w notacji naukowej i dwójkowo:

0.210=1.100112230.2_{10} = 1.1\overline{0011}_{2} \cdot 2^{-3}
 63 62        52 51                                                 0    ← pozycja bitu
 0  01111111100  1001100110011001100110011001100110011001100110011010
 │       │                                  │
sign  exponent                  mantissa (significand)

Obliczanie wyniku 0.1 + 0.2

Jeśli złożymy liczby z powrotem z ich reprezentacji zmiennoprzecinkowej do postaci naukowej, mamy tak:

0.1101.1001100110011001100110011001100110011001100110011010240.2101.100110011001100110011001100110011001100110011001101023\begin{aligned} 0.1_{10} &\approx 1.1001100110011001100110011001100110011001100110011010 \cdot 2^{-4} \\ 0.2_{10} &\approx 1.1001100110011001100110011001100110011001100110011010 \cdot 2^{-3} \end{aligned}

Aby dodawać liczby, muszą mieć równe wykładniki. Reguła mówi, że musimy dostosować liczbę o mniejszym wykładniku do tej o większym. Dostosujmy więc wykładnik -4 pierwszej liczby do wykładnika -3, jak u drugiej:

0.1100.11001100110011001100110011001100110011001100110011010230.1_{10} \approx 0.11001100110011001100110011001100110011001100110011010 \cdot 2^{-3}

Teraz możemy dodać liczby:

   0.1100110011001100110011001100110011001100110011001101
+  1.1001100110011001100110011001100110011001100110011010
   ─────────────────────────────────────────────────────────
  10.0110011001100110011001100110011001100110011001100111

Wynik obliczenia przechowywany jest w formacie zmiennoprzecinkowym, więc musimy go znormalizować, w razie potrzeby zaokrąglić i obliczyć wykładnik w kodzie z przesunięciem.

10.011001100110011001100110011001100110011001100110011123=1.001100110011001100110011001100110011001100110011001115322\begin{aligned} &10.0110011001100110011001100110011001100110011001100111 \cdot 2^{-3} = \\ &1.\underbrace{00110011001100110011001100110011001100110011001100111}_{53} \cdot 2^{-2} \end{aligned}

Znormalizowana liczba wypada dokładnie w środku między opcjami zaokrąglenia, więc stosujemy regułę rozstrzygania remisu i zaokrąglamy do parzystej. Daje to następującą liczbę wynikową w znormalizowanej postaci naukowej:

1.001100110011001100110011001100110011001100110011010052221.\underbrace{0011001100110011001100110011001100110011001100110100}_{52} \cdot 2^{-2}

A przekonwertowana na format zmiennoprzecinkowy do przechowania ma następujący układ bitów:

 63 62        52 51                                                 0    ← pozycja bitu
 0  01111111101  0011001100110011001100110011001100110011001100110100
 │       │                                  │
sign  exponent                  mantissa (significand)

To dokładnie ten układ bitów, który zostaje przechowany, gdy wykonasz instrukcję 0.1+0.2. Aby go uzyskać, komputer musi zaokrąglić trzy razy — po jednym razie dla każdej liczby i trzeci raz dla ich sumy. Gdy przechowywane jest po prostu 0.3, komputer zaokrągla tylko raz. Te operacje zaokrąglania prowadzą do różnych układów bitów przechowywanych dla 0.1+0.2 i dla samodzielnego 0.3.

Gdy język porównuje 0.1+0.2 z 0.3 — przez === w JavaScripcie, == w Pythonie — porównywane są właśnie te układy bitów, a skoro się różnią, zwracany wynik to false. Gdyby istniały takie formaty, w których nawet po zaokrągleniu układy bitów byłyby równe, porównanie dałoby true niezależnie od tego, że 0.1 i 0.2 nie są skończenie reprezentowalne dwójkowo.

Spróbuj sprawdzić bity liczby 0.3 za pomocą pokazanych wyżej funkcji — to64bitFloat(0.3) w JavaScripcie albo to64bit_float(0.3) w Pythonie. Układ będzie inny niż ten, który obliczyliśmy powyżej dla wyniku 0.1+0.2.

Aby odzyskać rzeczywistą wartość dziesiętną, którą te bity reprezentują, weź dwójkową postać naukową (niejawna 1, po niej 52 bity mantysy, pomnożone przez 2 do potęgi prawdziwego wykładnika), przesuń przecinek, wchłaniając wykładnik, i przekonwertuj powstały ułamek dwójkowy na dziesiętny. Wykonanie tej arytmetyki dla 0.1 + 0.2 daje 0.3000000000000000444089209850062616169452667236328125. Wykonanie jej dla 0.3 daje 0.299999999999999988897769753748434595763683319091796875. Są bliskie, ale nie równe — i właśnie dlatego porównanie zwraca false.

Gdzie języki się rozchodzą: opakowania ponad tym samym floatem

Sam float jest wspólny, ale języki nie zgadzają się w każdej operacji, która używa floata. Te rozejścia są decyzjami polityki na warstwie języka, nie różnicami w leżącej pod spodem arytmetyce. Kilka wartych poznania:

Dzielenie przez zero. JavaScript zwraca Infinity.

> 1 / 0
Infinity
> -1 / 0
-Infinity

Python rzuca ZeroDivisionError.

>>> 1 / 0
Traceback (most recent call last):
  ...
ZeroDivisionError: division by zero

Aby dostać Infinity w Pythonie, prosisz o to jawnie: math.inf albo float('inf'). Format zmiennoprzecinkowy ma całkiem dobre kodowanie nieskończoności (zobaczymy je niżej) — Python po prostu decyduje, żeby nie produkować jej z dosłownego 1/0.

Modulo liczb ujemnych. % w JavaScripcie idzie za znakiem dzielnej.

> -7 % 3
-1

% w Pythonie idzie za znakiem dzielnika.

>>> -7 % 3
2

Zaokrąglanie połówek. Math.round w JavaScripcie zaokrągla połówkę w górę (w stronę ++\infty).

> Math.round(2.5)
3
> Math.round(3.5)
4

Wbudowany round w Pythonie używa „zaokrąglania bankowego” — remisy do parzystej — co odpowiada domyślnemu zachowaniu IEEE-754 i regule, której używa wewnętrznie sam FPU.

>>> round(2.5)
2
>>> round(3.5)
4

Domyślne wyświetlanie dziesiętne. JavaScript pokazuje dokładnie tyle cyfr, by dało się wrócić do tego samego floata. repr() w Pythonie robi to samo. Oba ukrywają końcówkę …44089… przy 0.1 + 0.2, dopóki nie poprosisz o większą precyzję przez .toFixed(20) albo specyfikator formatu f"{x:.20f}".

Reguła praktyczna: gdy dwa języki nie zgadzają się co do liczby, podejrzewaj opakowanie, nie float. Te 64 bity pod spodem są takie same.

Granica 2532^{53} — w obu językach

Przechowywanie liczb całkowitych we floacie natrafia na twardy limit. Mantysa mieści 52 bity plus niejawną wiodącą 1, czyli 53 bity precyzji. Zatem każda liczba całkowita do 2532^{53} może być przechowana dokładnie. Dalej bitów mantysy nie wystarcza, by zakodować każdą całkowitą — i format musi niektóre pominąć.

JavaScript udostępnia tę granicę jako stałą:

> Number.MAX_SAFE_INTEGER
9007199254740991              // 2^53 - 1
> 2 ** 53
9007199254740992
> 2 ** 53 + 1
9007199254740992              // a nie 9007199254740993!
> 9007199254740992 === 9007199254740993
true

Wyrażenie 2 ** 53 + 1 nie daje 9007199254740993. Format nie potrafi przedstawić tej wartości, więc zaokrągla do najbliższej reprezentowalnej — czyli 9007199254740992.

Warto być precyzyjnym co do tego, co MAX_SAFE_INTEGER naprawdę oznacza: to nie jest największa liczba całkowita, jaką JavaScript może pomieścić. JavaScript potrafi przechować znacznie większe wartości — Number.MAX_VALUE dociąga aż do 1.7976931348623157e+308, największego skończonego double’a. MAX_SAFE_INTEGER w rzeczywistości oznacza największą całkowitą N, dla której zarówno N, jak i N + 1 są dokładnie reprezentowalne. Dalej zaczynają pojawiać się luki: MAX_SAFE_INTEGER + 3 (czyli 9007199254740994) jest w porządku, ale MAX_SAFE_INTEGER + 2 (czyli 9007199254740993) to pierwsza całkowita, której format nie umie przedstawić — wpisz ją w konsolę i dostaniesz z powrotem 9007199254740992, po cichu zaokrąglone w dół o 1. Gdy wykładnik rośnie dalej, luki się rozszerzają, więc gdy zbliżysz się do MAX_VALUE, kolejne reprezentowalne wartości mogą być od siebie oddalone o ogromne odległości.

Jeśli naprawdę potrzebujesz dokładnej arytmetyki całkowitej za MAX_SAFE_INTEGER w JavaScripcie, to właśnie do tego jest BigInt — ma dowolną precyzję, więc granica znika całkowicie:

> 9007199254740993n
9007199254740993n            // BigInt, zostaje dokładny — sufiks n ma znaczenie
> 9007199254740993n === 9007199254740992n
false                        // różne wartości, w odróżnieniu od Number
> 2n ** 53n + 1n
9007199254740993n            // każda operacja arytmetyczna na BigInt zostaje dokładna
> 2n ** 1000n                // i nie ma górnego limitu
107150860718626732094842504906000181056140481170553360744375038837035105112493612249319837881569585812759467291755314682518714528569231404359845775746985748039345677748242309854210746050623711418779541821530464749835819412673987675591655439460770629145711964776865421676604298316526243868372056680693100n

Ta sama granica dokładnego przechowywania całkowitych we floacie istnieje też w Pythonie, ale jest niewidoczna, dopóki nie przejdziesz z int na float. Tak jak BigInt w JavaScripcie, int w Pythonie ma dowolną precyzję, więc arytmetyka całkowita jest dokładna:

>>> 2**53 + 1
9007199254740993                # dokładnie, bo obie strony są int
>>> 2**53 + 1 == 2**53
False

Ale w chwili, gdy rzutujesz na float, wchodzi ten sam 64-bitowy format i ta sama granica:

>>> 2.0**53 + 1.0
9007199254740992.0
>>> 2.0**53 + 1 == 2.0**53
True
>>> float(9007199254740993) == float(9007199254740992)
True

Te same bity, ta sama granica. JavaScript udostępnia ją jako stałą najwyższego poziomu, bo każdy zwykły literał liczbowy w JavaScripcie jest floatem — BigInt doszedł później jako wyjście awaryjne, ale wymaga jawnego sufiksu n, by się na niego zdecydować — więc język oznacza granicę nazwą, żeby Cię ostrzec. Python odwraca domyślne ustawienia: goły literał całkowity jest już int-em o dowolnej precyzji, więc granica jest niewidoczna, dopóki jawnie nie rzutujesz na float. Dlatego MAX_SAFE_INTEGER nie pojawia się w bibliotece standardowej Pythona — w kodzie operującym tylko na int nigdy nie ma znaczenia.

Dlaczego dokładnie 2532^{53}?

Spójrz na układ bitów liczby 9007199254740991 (czyli 25312^{53} - 1). To liczba całkowita, której reprezentacja dwójkowa to pięćdziesiąt trzy 1 z rzędu, znormalizowana jako

1.111152 jedynki×2521.\underbrace{11\ldots11}_{52 \text{ jedynki}} \times 2^{52}

co całkowicie wypełnia mantysę jedynkami. Aby przechować następną liczbę całkowitą — 2532^{53} — dodajemy 1, co przetacza przeniesienie przez wszystkie 52 bity mantysy (każda 1 przeskakuje na 0), zostawiając nas z

10.000052 zera×25210.\underbrace{00\ldots00}_{52 \text{ zera}} \times 2^{52}

Ponowna normalizacja przesuwa przecinek o jedno miejsce w lewo i zwiększa wykładnik o jeden, dając mantysę z samych zer:

1.000052 zera×253=9,007,199,254,740,9921.\underbrace{00\ldots00}_{52 \text{ zera}} \times 2^{53} = 9{,}007{,}199{,}254{,}740{,}992

Dla następnej całkowitej, 253+12^{53} + 1, potrzebowalibyśmy ustawionego bitu mantysy na pozycji 53 — ale mantysa ma tylko 52 bity szerokości. Nie ma miejsca. Format po cichu zaokrągla do najbliższej reprezentowalnej wartości (albo 2532^{53}, albo 253+22^{53} + 2, zależnie od rozstrzygania remisu; dla +1 wypada na 2532^{53}).

Mechaniczna przyczyna, dla której tak się dzieje: przy wykładniku 52 mantysa dokładnie obejmuje każdą liczbę całkowitą do 2532^{53}. Aby przechować cokolwiek większego, musimy podnieść wykładnik — ale podniesienie go do 53 znaczy, że przecinek przesuwa się o 53 miejsca w prawo, podczas gdy mantysa nadal daje nam tylko 52 przechowane bity, więc 53. pozycja bitowa jest zawsze niejawnie 0. Przy wykładniku 54 format dopisuje dwa zera, przy 55 trzy, i tak dalej.

Aby zobaczyć to konkretnie, spójrz na pozycje bitowe kilku pierwszych liczb całkowitych za 2532^{53}. Każda ma 54 bity szerokości (od bitu 53 do bitu 0), ale format ma miejsce tylko na 53 z nich — niejawną wiodącą 1 plus 52 przechowane bity mantysy. Najmniej znaczący bit (skrajnie prawy, bit 0 — nazwijmy go LSB) nie ma miejsca, więc format fizycznie nie może postawić tam 1. Wynikają z tego dwie konsekwencje:

  1. W tym zakresie reprezentowalne są tylko całkowite z LSB=0 (czyli parzyste).
  2. Sąsiedzi są od siebie o 2, więc przyrost +1 zaokrągla z powrotem do tej samej wartości — potrzebujesz co najmniej +2, by wylądować na następnej reprezentowalnej całkowitej. (A +1 jest dokładnie w połowie drogi między dwoma sąsiadami, więc reguła remisu do parzystej wybiera tego, którego mantysa kończy się na 0.)
2^53     = 9007199254740992 = 1 0000…0000 0   ← LSB=0 (parz.)    ✓ reprezentowalna
2^53 + 1 = 9007199254740993 = 1 0000…0000 1   ← LSB=1 (nieparz.) ✗ brak miejsca na LSB
2^53 + 2 = 9007199254740994 = 1 0000…0001 0   ← LSB=0 (parz.)    ✓ reprezentowalna
2^53 + 3 = 9007199254740995 = 1 0000…0001 1   ← LSB=1 (nieparz.) ✗ brak miejsca na LSB
                              ↑ └─── 52 ───┘ ↑
                              │   mantissa   │
                              │    bits      │
                              │              └── LSB nie ma slotu w mantysie
                              │                  → zawsze 0 → całkowita zawsze parzysta
                              └── niejawna wiodąca 1 (nieprzechowywana)

Wniosek jest zaskakujący: każda liczba całkowita większa od MAX_SAFE_INTEGER kończy się co najmniej jednym niejawnym zerem, więc żadnej nieparzystej liczby całkowitej powyżej MAX_SAFE_INTEGER nie da się w ogóle przedstawić — tylko parzyste. Wspinaj się po wykładniku dalej i reprezentowalne będą tylko wielokrotności 4, potem wielokrotności 8, potem 16 — luka podwaja się na każdym kroku. Możesz to obejrzeć wprost w którymkolwiek REPL-u:

2^53                  → 9007199254740992
2^53 + 1 == 2^53      → true   (luka tutaj 2, +1 się zapada)
2^53 + 2  > 2^53      → true   (+2 to następna reprezentowalna)

2^54 + 2 == 2^54      → true   (luka 4, nawet +2 się zapada)
2^54 + 4  > 2^54      → true   (+4 to następna)

2^55 + 4 == 2^55      → true   (luka teraz 8)
2^55 + 8  > 2^55      → true   (+8 to następna)

Każdy krok podwaja lukę. Gdy dojdziesz do największego skończonego double’a (Number.MAX_VALUE albo sys.float_info.max, oba około 1.8×103081.8 \times 10^{308}), kolejne reprezentowalne wartości będą od siebie oddalone o ogromne odległości.

Pułapka pętli for

Zachowanie z rozszerzającymi się lukami, które właśnie przeszliśmy, ma słynną praktyczną konsekwencję — pętlę, która wygląda, jakby powinna się zakończyć, ale nigdy tego nie robi. Weźmy najprostszą możliwą pętlę „zwiększaj, dopóki odwrotność nie będzie zerem”, napisaną w obu językach.

Jeśli uruchomimy ją w JavaScripcie:

for (let i = 1; 1/i > 0; i++) {
  console.log("Count is: " + i);
}

albo w Pythonie:

i = 1.0
while 1/i > 0:
    print(f"Count is: {i}")
    i += 1

Pętla nigdy się nie zatrzymuje. To zachowanie jest wspólne dla obu języków, bo oba liczniki są 64-bitowymi floatami podlegającymi granicy bezpiecznych całkowitych. Warunek 1/i > 0 jest prawdziwy dla każdego skończonego dodatniego i, więc żeby pętla się zatrzymała, licznik musiałby dojść do Infinity. Ale gdy i dochodzi do 2532^{53}, i + 1 zaokrągla się z powrotem do 2532^{53} — luka do następnej reprezentowalnej wartości to 2, a +1 wypada w środku i zaokrągla w dół. Licznik zostaje na 9007199254740992 na zawsze, 1/i pozostaje maleńką liczbą dodatnią, a pętla kręci się w miejscu.

Możesz pomyśleć, że zwiększanie o 2 ominie ten problem — i przez jakiś czas tak jest. Z i += 2 licznik przeskakuje 2532^{53} i dalej kroczy po reprezentowalnych parzystych (2^53, 2^53 + 2, 2^53 + 4, …). Ale gdy dojdzie do 2542^{54}, luka podwaja się do 4, więc i + 2 zaokrągla z powrotem do i i pętla znów zostaje unieruchomiona. Przejście na i += 4 tylko odkłada pułapkę do 2552^{55}, gdzie luka staje się 8, i tak dalej. Fundamentalny problem nie leży w +1 — chodzi o to, że przy wspinaniu się w górę kolejne liczby całkowite przestają być reprezentowalne, a każdy licznik o stałym kroku w końcu wpadnie w lukę większą niż jego krok.

Sedno: ta pułapka jest własnością Number / float, nie języka. Może na nią trafić każdy język, który używa double’a IEEE-754 jako typu licznika.

NaN i Infinity

Format rezerwuje dwie wartości wykładnika na specjalne kodowania:

  • Wykładnik z samych zer jest zarezerwowany dla ±0 (gdy mantysa też jest zerowa) i dla liczb zdenormalizowanych (gdy mantysa jest niezerowa — wartości zbyt małe, by przedstawić je w postaci znormalizowanej).
  • Wykładnik z samych jedynek jest zarezerwowany dla ±Infinity (mantysa zerowa) i NaN (mantysa niezerowa).

Dlatego typeof NaN w JavaScripcie to 'number', a type(float('nan')) w Pythonie to <class 'float'> — NaN jest całkowicie poprawnym układem bitów formatu zmiennoprzecinkowego, tylko z wykładnikiem z samych jedynek.

Układ bitów dodatniej nieskończoności to

0 11111111111 0000000000000000000000000000000000000000000000000000

A typowy NaN wygląda tak

0 11111111111 1000000000000000000000000000000000000000000000000000

Widzimy, że każda niezerowa mantysa w parze z wykładnikiem z samych jedynek koduje NaN — co znaczy, że odrębnych układów bitów NaN jest całkiem dużo, a nie tylko jeden.

Reguła IEEE-754 mówi, że NaN jest różny od wszystkiego, włącznie z samym sobą. Oba języki tego przestrzegają. Oto JavaScript:

> NaN === NaN
false
> NaN > 0
false
> NaN < 0
false
> Number.isNaN(NaN)
true

I to samo zachowanie w Pythonie:

>>> import math
>>> float('nan') == float('nan')
False
>>> float('nan') > 0
False
>>> float('nan') < 0
False
>>> math.isnan(float('nan'))
True

Oba języki dostarczają dedykowane isNaN / math.isnan, bo — zgodnie z regułą IEEE-754 — x == x to zły sposób wykrywania NaN: zwraca false dla NaN i true dla wszystkiego innego, ale jest to raczej sztuczka na odwrotnej logice niż czyste API. (Właściwie x !== x jest częstą idiomą w starszym kodzie JS, właśnie dlatego, że jednoznacznie identyfikuje NaN.)

Infinity podlega zwykłym regułom porównywania — jest większa od każdej liczby skończonej, mniejsza od siebie samej tylko w sensie równości — i można mieszać ją w arytmetyce bez zaskoczeń (Infinity + 1 to Infinity, 1 / Infinity to 0, Infinity - Infinity to NaN).

> Infinity > 1e308
true
> Infinity + 1 === Infinity
true
> Infinity - Infinity
NaN

I to samo zachowanie w Pythonie:

>>> math.inf > 1e308
True
>>> math.inf + 1 == math.inf
True
>>> math.inf - math.inf
nan

Co warto zabrać ze sobą

  1. Niemal każdy nowoczesny język używa tych samych 64 bitów dla liczb ułamkowych. Number JavaScriptu, float Pythona, double Javy, double C — wszystko to IEEE-754 binary64. Ten sam układ, ta sama arytmetyka, te same dziwactwa. Różnice między językami żyją w opakowaniu, nie we floacie.
  2. 0.1 + 0.2 != 0.3 jest własnością formatu. Dzieje się tak, bo 0.1 i 0.2 nie są skończone dwójkowo i kumulują trzy błędy zaokrąglenia podczas dodawania; przechowanie 0.3 wprost kumuluje tylko jeden. Układy bitów się różnią.
  3. Granica „bezpiecznej liczby całkowitej” leży na 2532^{53}. Za nią nie wszystkie wartości całkowite dają się przedstawić, a sąsiednie całkowite zapadają się na tego samego floata. JavaScript udostępnia ją jako Number.MAX_SAFE_INTEGER, bo nie ma innego typu całkowitego; użytkownicy Pythona widzą ją tylko po rzutowaniu na float.
  4. NaN i Infinity to prawdziwe układy bitów, nie wyjątki. Żyją wewnątrz formatu, a zarezerwowany dla nich jest wykładnik z samych jedynek. Reguła „NaN jest różny od wszystkiego, włącznie z samym sobą” pochodzi z IEEE-754, nie jest wyborem języka.

Znając te cztery rzeczy, potrafisz przewidzieć, co zrobi float w każdym języku, który go używa — czyli, w praktyce, we wszystkich.

Głębsze zanurzenie w to, jak cztery podstawowe operacje (dodawanie, odejmowanie, mnożenie, dzielenie) faktycznie przekształcają bity — zarówno dla liczb całkowitych w uzupełnieniu do dwóch, jak i dla liczb IEEE-754 — znajdziesz w artykule Jak działa arytmetyka dwójkowa: liczby całkowite w uzupełnieniu do dwóch i liczby IEEE-754.