Jak Number w JavaScripcie i float w Pythonie przechowują liczby
Wiele języków statycznie typowanych oferuje kilka typów liczbowych. Java ustala szerokość byte, int i long na 8, 32 i 64 bity, a float i double na 32 i 64 bity. W C rozmiary zależą od implementacji: zwykły char może mieć znak lub go nie mieć, a long nie zawsze ma 64 bity. Tutaj skupiamy się na IEEE-754 binary64, używanym przez JavaScriptowy Number i Pythonowy float na typowych platformach.
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
falseTe same wyrażenia w Pythonie — te same odpowiedzi, co do bitu:
>>> 9007199254740992.0 == 9007199254740993.0
True
>>> float('nan') == float('nan')
FalseW 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 reguł samego języka.
Jakie typy liczbowe udostępnia każdy z języków
Zanim przyjrzymy się bitom, sprawdźmy, jakie typy udostępnia każdy język. Oba mają oddzielne typy dla liczb całkowitych i ułamkowych, o różnych nazwach i różnych domyślnych ustawieniach:
| Język | Rodzaj | Typ | Szczegóły |
|---|---|---|---|
| Python | Całkowita (bez części dziesiętnej, dokładna) | int | Dowolna precyzja. CPython zwiększa przydzieloną pamięć w miarę potrzeby, więc 2**1000 po prostu działa, a wynik jest dokładny. |
| Zmiennoprzecinkowa (64-bitowy double IEEE-754) | float | Sama wartość binary64 zajmuje 8 bajtów; obiekt float w Pythonie wymaga dodatkowego miejsca na metadane. | |
| JavaScript | Całkowita (bez części dziesiętnej, dokładna) | BigInt | Liczba 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) | Number | Ten 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. |
Literały całkowite Pythona, takie jak 5, tworzą int. Kropka dziesiętna lub znacznik wykładnika oznacza literał zmiennoprzecinkowy: zarówno 5.2, jak i 5e0 tworzą float.
>>> 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
6Python domyślnie przypisuje literałom całkowitym typ int, a JavaScript literałom liczbowym bez sufiksu — Number. Dalej skupiamy się na stronie zmiennoprzecinkowej: Pythonowym float, JavaScriptowym Number i ich reprezentacji binary64.
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 (liczba Avogadra) albo (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:
Część znacząca to czynnik mnożony przez podstawę podniesioną do wykładnika; precyzja to liczba dostępnych cyfr lub bitów znaczących. Część znaczącą często nazywa się „mantysą”; poniżej używamy tego określenia także nieformalnie dla zapisanego pola ułamka. Zera wewnątrz części znaczącej mogą być istotne: 1.01 różni się od 1.1. Podstawa wynosi 10 w zapisie dziesiętnym i 2 w binarnym, a wykładnik określa skalę.
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:
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:
Mnożymy przez , więc wartość pozostaje bez zmian. Mnożenie przez przesuwa przecinek o osiem miejsc w prawo i daje . Pozostały czynnik kompensuje to przesunięcie.
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:
Pomnożyliśmy przez , więc kompensujemy to czynnikiem . Dla 22300000 dzielenie przez daje z kolei część znaczącą 223:
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:
Przedstawienie liczb w postaci znormalizowanej umożliwia łatwe porównywanie ich rzędów 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:
| Nazwa | Bitów łącznie | Wykładnik | Zapisane bity ułamka |
|---|---|---|---|
| Pojedyncza precyzja | 32 | 8 | 23 |
| Podwójna precyzja | 64 | 11 | 52 |
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łkowitejPrzejdź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:
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 bity. Ta funkcja pomocnicza w JavaScripcie zapisuje liczbę przez DataView w kolejności big-endian, a następnie formatuje bajty w tej kolejności. Jawny porządek bajtów uniezależnia wynik od architektury komputera:
function to64bitFloat(number) {
const buffer = new ArrayBuffer(8);
new DataView(buffer).setFloat64(0, number, false);
return Array.from(new Uint8Array(buffer),
byte => byte.toString(2).padStart(8, '0')).join('');
}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.
Kluczowe jest to, ż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 1 — wię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 są same zera, 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:
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):
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:
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 rosnące od prawej. Bity liczb całkowitych skupiają się po prawej, bo tam mieszkają najmniejsze wielkości ().
- Mantysa zmiennoprzecinkowa: bity stoją po przecinku w postaci znormalizowanej i reprezentują potęgi malejące od przecinka. Skrajnie lewy przechowany bit to , następny i tak dalej. Zatem mantysa dla przechowuje
11od lewej do prawej, kładąc pierwszą1na , a drugą na — 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.
Po skróceniu do postaci nieskracalnej ułamek ma skończone rozwinięcie binarne dokładnie wtedy, gdy mianownik jest potęgą dwójki. Dlatego 0.1 i 0.2 trzeba przybliżyć w binarnym formacie zmiennoprzecinkowym. W binary64 z zaokrąglaniem remisów do parzystej ich suma różni się od zapisanej wartości 0.3. Sama nieskończoność rozwinięcia nie dowodzi nierówności dla każdej precyzji; decydują kroki zaokrąglania.
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...Następnym krokiem jest przedstawienie tej liczby w znormalizowanej notacji naukowej:
Ponieważ mantysa może mieć tylko 52 bity, musimy zaokrąglić nieskończone rozwinięcie tej liczby do 52 bitów po przecinku.
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:
Ostatnią rzeczą jest obliczenie reprezentacji wykładnika w kodzie z przesunięciem:
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:
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:
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:
Teraz możemy dodać liczby:
0.1100110011001100110011001100110011001100110011001101
+ 1.1001100110011001100110011001100110011001100110011010
─────────────────────────────────────────────────────────
10.0110011001100110011001100110011001100110011001100111Wynik 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.
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:
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 zgodnie z wykładnikiem, 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.
Różnice między językami używającymi tego samego formatu zmiennoprzecinkowego
Sam float jest wspólny, ale języki nie zgadzają się w każdej operacji, która używa floata. Te różnice wynikają z reguł ustalonych na poziomie języka, a nie z różnic w samej arytmetyce. Kilka wartych poznania:
Dzielenie przez zero. JavaScript zwraca Infinity.
> 1 / 0
Infinity
> -1 / 0
-InfinityPython rzuca ZeroDivisionError.
>>> 1 / 0
Traceback (most recent call last):
...
ZeroDivisionError: division by zeroAby 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
2Zaokrąglanie połówek. Math.round w JavaScripcie zaokrągla połówkę w górę (w stronę ).
> Math.round(2.5)
3
> Math.round(3.5)
4Wbudowany 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)
4Domyś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, sprawdź reguły języka, a nie tylko format zmiennoprzecinkowy. Te 64 bity pod spodem są takie same.
Granica — 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 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
trueWyraż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 // dokładne dodawanie liczb całkowitych
> 2n ** 1000n // zakres ograniczają zasoby i implementacja
10715086071862673209484250490600018105614048117055336074437503883703510511249361224931983788156958581275946729175531468251871452856923140435984577574698574803934567774824230985421074605062371141877954182153046474983581941267398767559165543946077062914571196477686542167660429831652624386837205668069376nTa 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
FalseAle 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)
TrueTe 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 ?
Spójrz na układ bitów liczby 9007199254740991 (czyli ). To liczba całkowita, której reprezentacja dwójkowa to pięćdziesiąt trzy 1 z rzędu, znormalizowana jako
co całkowicie wypełnia mantysę jedynkami. Aby przechować następną liczbę całkowitą — — dodajemy 1, co przetacza przeniesienie przez wszystkie 52 bity mantysy (każda 1 przeskakuje na 0), zostawiając nas z
Ponowna normalizacja przesuwa przecinek o jedno miejsce w lewo i zwiększa wykładnik o jeden, dając mantysę z samych zer:
Dla następnej całkowitej, , 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 , albo , zależnie od rozstrzygania remisu; dla +1 wypada na ).
Mechaniczna przyczyna, dla której tak się dzieje: przy wykładniku 52 mantysa dokładnie obejmuje każdą liczbę całkowitą do . 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 . 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:
- W tym zakresie reprezentowalne są tylko parzyste liczby całkowite.
- Sąsiedzi są oddaleni o
2. Dodanie1daje remis: wynik może wrócić do pierwotnej wartości lub wzrosnąć do kolejnej parzystej, zależnie od ostatniego zachowanego bitu. Na przykład(2 ** 53 + 2) + 1zaokrągla się w górę do2 ** 53 + 4.
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: Tutaj ^ oznacza potęgowanie w zapisie matematycznym. W JavaScript użyj **, a w Pythonie podstawy zmiennoprzecinkowej, np. 2.0 ** 53, aby zaobserwować zaokrąglanie.
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 ), 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 += 1Pę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 , i + 1 zaokrągla się z powrotem do — 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 i dalej kroczy po reprezentowalnych parzystych (2^53, 2^53 + 2, 2^53 + 4, …). Ale gdy dojdzie do , 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 , 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 licznik o małym, stałym kroku całkowitym 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 0000000000000000000000000000000000000000000000000000A typowy NaN wygląda tak
0 11111111111 1000000000000000000000000000000000000000000000000000Widzimy, ż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)
trueI to samo zachowanie w Pythonie:
>>> import math
>>> float('nan') == float('nan')
False
>>> float('nan') > 0
False
>>> float('nan') < 0
False
>>> math.isnan(float('nan'))
TrueUżyj Number.isNaN(x) w JavaScript lub math.isnan(x) dla liczb float w Pythonie, aby wyrazić ten test wprost. Starsza globalna funkcja JavaScript isNaN dodatkowo konwertuje argument na liczbę, więc może zwrócić true dla napisu. Nierówność z samym sobą (x !== x w JavaScript, x != x dla float w Pythonie) również wykrywa NaN; jest poprawna, choć mniej czytelna.
Dodatnie Infinity jest większe od każdej liczby skończonej i równe sobie. Jego arytmetyka ma przypadki szczególne: Infinity + 1 daje Infinity, 1 / Infinity daje 0, a Infinity - Infinity daje NaN.
> Infinity > 1e308
true
> Infinity + 1 === Infinity
true
> Infinity - Infinity
NaNI to samo zachowanie w Pythonie:
>>> math.inf > 1e308
True
>>> math.inf + 1 == math.inf
True
>>> math.inf - math.inf
nanCo warto zabrać ze sobą
- JavaScriptowy
Numberi typowy Pythonowyfloatużywają binary64, z 53 bitami precyzji dla liczb normalnych. - W tym formacie zaokrąglenie
0.1,0.2i ich sumy daje inną wartość niż bezpośrednie zaokrąglenie0.3. - Binary64 reprezentuje wszystkie liczby całkowite od do , ale bezpieczny zakres JavaScriptu kończy się na module . Do dokładnych większych liczb użyj
BigIntlub Pythonowegoint. - NaN i nieskończoności mają zarezerwowane kody. Języki nadal mogą różnie obsługiwać operacje takie jak dzielenie przez zero.
Te cztery fakty wyjaśniają wiele typowych niespodzianek przy pracy z liczbami zmiennoprzecinkowymi binary64.
Dokładniejsze omówienie tego, 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.