Kod z przesunięciem a uzupełnienie do dwóch: dwa sposoby reprezentowania liczb ze znakiem

Liczby ze znakiem można przedstawiać binarnie na kilka sposobów, każdy z innymi kompromisami. Kod uzupełnień do dwóch jest typowy dla całkowitych liczb ze znakiem o stałej szerokości na współczesnych procesorach. Znak-moduł oddziela znak od wartości bezwzględnej; takie podejście występuje też w formatach zmiennoprzecinkowych. Kod z przesunięciem, czyli excess-K, dodaje stałe przesunięcie; formaty binarne IEEE-754 używają go do wykładników liczb znormalizowanych. Porównamy te trzy sposoby kodowania.

Naiwna pierwsza próba: kod znak-moduł

Najbardziej intuicyjny sposób rozszerzenia zapisu binarnego na liczby ze znakiem to zarezerwowanie skrajnego lewego bitu jako flagi znaku — 0 dla dodatnich, 1 dla ujemnych — i zakodowanie modułu pozostałymi bitami jako liczby bez znaku. Ten schemat nazywa się kodem znak-moduł.

Na przykładzie 4 bitów +5 i -5 wyglądają tak:

+510=01012510=11012\begin{aligned} +5_{10} &= 0101_2 \\ -5_{10} &= 1101_2 \end{aligned}

Moduł 101 pozostaje ten sam; zmienia się tylko bit znaku. Zapis jest czytelny, ale wymaga dodatkowej obsługi w arytmetyce całkowitoliczbowej.

Dwa zera

Ponieważ bit znaku jest niezależny od modułu, istnieją dwie reprezentacje zera:

+010=00002010=10002\begin{aligned} +0_{10} &= 0000_2 \\ -0_{10} &= 1000_2 \end{aligned}

Zero powinno być jedną rzeczą. Dwa wzorce bitowe dla niego zmuszają procesor do specjalnego traktowania sprawdzania równości, gdzie 0000 == 1000 musi wciąż dawać prawdę. Marnuje to również jeden z 16 możliwych wzorców na 4 bitach — w schemacie znak-moduł mamy tylko 15 różnych wartości w zakresie [-7; 7], a nie pełne 16.

Arytmetyka nie działa sama z siebie

W zapisie binarnym bez znaku a + b jest proste — dodajesz bit po bicie, propagujesz przeniesienia, gotowe. Kod znak-moduł to psuje. Rozważmy 3 + (-3) w 4-bitowym kodzie znak-moduł:

100112(+3)+  110112(3)111102(?)\begin{aligned} & \phantom{1}0011_2 \quad (+3) \\ +\; & \phantom{1}1011_2 \quad (-3) \\ \hline & \phantom{1}1110_2 \quad (?) \end{aligned}

Czytając 1110 jako znak-moduł, dostajemy -6 — kompletnie błędnie. Zwykłe dodawanie binarne nie wie, że najstarszy bit ma być znakiem; dodaje go razem ze wszystkim innym. Aby arytmetyka znak-moduł działała, procesor musi zbadać bity znaku obu argumentów, zdecydować, czy dodawać, czy odejmować moduły, być może porównać moduły, by ustalić znak wyniku, i osobno obsłużyć przypadek dwóch zer. Wszystko to przekłada się na dodatkową logikę obsługi znaku.

To właśnie te problemy zmotywowały przejście do schematu, w którym znak nie jest osobną flagą, lecz arytmetyczną konsekwencją samego kodowania — do uzupełnienia do dwóch.

Szybkie wprowadzenie do uzupełnienia do dwóch

Uzupełnienie do dwóch to standardowe kodowanie liczb całkowitych ze znakiem w nowoczesnych procesorach, a jest prostsze, niż brzmi.

W uzupełnieniu do dwóch skrajny lewy bit (najbardziej znaczący) odgrywa podwójną rolę: uczestniczy w obliczeniach jak każdy inny bit, a także — gdy wartość interpretuje się jako liczbę całkowitą ze znakiem — mówi Ci, jaki jest znak (0 dla nieujemnych, 1 dla ujemnych). Nie jest to oddzielnie interpretowana flaga; znak wynika z arytmetyki, bo waga pozycyjna najstarszego bitu jest ujemna.

Weźmy liczbę binarną 1011 i założmy, że jest to liczba całkowita ze znakiem w uzupełnieniu do dwóch. Aby ją zdekodować, używamy podobnej notacji pozycyjnej jak w zapisie binarnym bez znaku, gdzie każdemu bitowi przypisuje się jako wagę potęgę dwójki — ale waga najstarszego bitu ma znak minus. Dla 4 bitów wygląda to tak:

b3(23)+b222+b121+b020b_3 \cdot (-2^3) + b_2 \cdot 2^2 + b_1 \cdot 2^1 + b_0 \cdot 2^0

Zwróć uwagę, co się zmieniło względem zwykłego binarnego bez znaku: trzy niższe bity b2,b1,b0b_2, b_1, b_0 zachowują swoje zwykłe dodatnie wagi (+4+4, +2+2, +1+1), ale najstarszy bit b3b_3 jest mnożony przez 23=8-2^3 = -8 zamiast +23=+8+2^3 = +8. Ta jedna zmiana znaku na najwyższej pozycji to cały schemat. Generalizując: dla n-bitowej liczby w uzupełnieniu do dwóch najstarszy bit ma wagę 2n1-2^{n-1} zamiast zwykłego +2n1+2^{n-1}, a wszystkie pozostałe bity zachowują swoje normalne dodatnie wagi.

Podstawiając 1011 do wzoru, otrzymujemy 8+0+2+1=5-8 + 0 + 2 + 1 = -5 — a nie 11 (co dostałbyś, czytając 1011 jako zwykłe binarne bez znaku: 8+0+2+18 + 0 + 2 + 1) i nie -3 (co dałby kod znak-moduł: najstarszy bit 1 oznaczający liczbę ujemną, niższe bity 011 kodujące moduł 3).

Jak zakodować liczbę ujemną

W poprzedniej sekcji zdekodowaliśmy wzorzec bitowy, który już siedział w pamięci. Ale jak przejść w drugą stronę — zacząć od liczby dziesiętnej takiej jak -5 i utworzyć 4-bitowy wzorzec, który ją przechowuje? Wzór pozycyjny mówi, co znaczy dany wzorzec, ale nie mówi wprost, jak zbudować wzorzec dla zadanej wartości ujemnej.

Na szczęście istnieje prosty przepis, który za każdym razem daje właściwy wzorzec: weź dodatnią wersję liczby, zaneguj wszystkie bity (0 → 1, 1 → 0) i dodaj 1. Wynik jest zapisem tej ujemnej wartości w uzupełnieniu do dwóch. (Dlaczego ten przepis działa, zobaczymy później, w sekcji o arytmetyce modularnej — na razie potraktuj go jako procedurę krok po kroku.)

Użyjmy go, by znaleźć reprezentację -5 na 4 bitach:

  1. Zacznij od zapisu binarnego +5:
+510=01012+5_{10} = 0101_2
  1. Zaneguj wszystkie bity:
01012101020101_2 \rightarrow 1010_2
  1. Dodaj 1:
10102+00012=101121010_2 + 0001_2 = 1011_2

Zatem -5 jest przechowywane jako 1011 w 4-bitowym uzupełnieniu do dwóch. Sprawdźmy wzorem wag bitowych: 8+0+2+1=5-8 + 0 + 2 + 1 = -5.

Dlaczego stosuje się ten schemat

Uzupełnienie do dwóch pozwala procesorowi używać tego samego układu dodawania (fizycznego bloku sprzętowego z bramek logicznych, który wykonuje dodawanie bit po bicie z propagacją przeniesienia) zarówno dla arytmetyki ze znakiem, jak i bez znaku.

Wykonajmy dodawanie 5 + (-5):

101012(+5)+  110112(5)100002\begin{aligned} & \phantom{1}0101_2 \quad (+5) \\ +\; & \phantom{1}1011_2 \quad (-5) \\ \hline & 10000_2 \end{aligned}

Przeniesienie z najstarszego bitu jest odrzucane, pozostaje 0000 — dokładnie zero. Sprzęt nie musi sprawdzać, czy argumenty są ze znakiem, i nie potrzebuje osobnej ścieżki odejmowania. Zwykłe dodawanie binarne po prostu działa.

Z tego kodowania wynikają jeszcze dwie właściwości. Po pierwsze, zakres dla n bitów to [2n1; 2n11][-2^{n-1};\ 2^{n-1}-1] — dla 4 bitów [-8; 7], dla 8 bitów [-128; 127]. Zauważ, że jest asymetryczny: o jedną wartość ujemną więcej niż dodatnich, bo wzorzec, który w schemacie znak-moduł reprezentowałby -0, jest tu przeznaczony na najbardziej ujemną liczbę. Po drugie, istnieje dokładnie jeden wzorzec zera 0000, a nie dwa: 0000 i 1000.

Zasada spajająca: arytmetyka modularna

Przyjrzyjmy się teraz, dlaczego uzupełnienie do dwóch działa tak, jak działa. Jedna idea, która wyjaśnia niemal wszystko w tym formacie, brzmi: arytmetyka n-bitowa to arytmetyka modularna na okręgu o 2n2^n pozycjach. Niemal każda inna właściwość formatu — przepis „zaneguj i dodaj 1”, argument o jednym układzie, zawijanie przy przepełnieniu — wynika bezpośrednio z tego jednego ujęcia.

Co znaczy „arytmetyka modularna”

Dodawanie, odejmowanie i mnożenie n-bitowe z zawijaniem wyniku działają modulo 2n2^n. Wyobraź sobie 4-bitowy zegar o 16 pozycjach, z których każda ma dwie etykiety: wartość bez znaku (na zewnątrz) i wartość ze znakiem w uzupełnieniu do dwóch (wewnątrz). To opis wyniku na poziomie bitów; reguły języka mogą być inne — w C przepełnienie liczb ze znakiem jest zachowaniem niezdefiniowanym:

zewnątrz: bez znaku (0–15)wewnątrz: ze znakiem (−8…+7)te same bity, dwa odczyty001+12+23+34+45+56+67+78−89−710−611−512−413−314−215−1

Zwróć uwagę, jak etykiety ze znakiem dzielą okrąg na dwie połowy: prawa strona mieści wartości nieujemne 0+7, lewa strona wartości ujemne −1−8 (przy czym sama pozycja 8 — wzorzec bitowy 1000 — mieści wartość najbardziej ujemną, −8).

Kilka rzeczy warto podkreślić w działaniu tego zegara:

  • Każda pozycja mieści jeden wzorzec bitowy — etykiety bez znaku i ze znakiem to tylko dwie różne interpretacje tych samych 4 bitów.
  • Dodanie 1 przesuwa Cię o jedną pozycję zgodnie z ruchem wskazówek zegara.
  • Za 15 zawijasz z powrotem do 0. Na tym zegarze 15 + 1 = 0, 15 + 2 = 1, 15 + 3 = 2 i tak dalej — każda wartość za 15 idzie dalej po okręgu, trafiając na pozycję 0, potem 1, potem 2.

Matematycy mają zwięzłą notację dla tego zawijania: a ≡ b (mod n) czyta się jako „a trafia na tę samą pozycję zegara co b na zegarze o n pozycjach”. Nasze przykłady zawijania z punktu powyżej można więc zapisać jako 16 ≡ 0 (mod 16), 17 ≡ 1 (mod 16), 18 ≡ 2 (mod 16) — każdy po prostu mówi, że liczba po lewej trafia na tę samą pozycję co liczba po prawej, gdy idziesz po okręgu o 16 pozycjach. Zatem 18 ≡ 2 (mod 16) mówi tylko tyle, że 18 trafia na tę samą pozycję co 2 na 16-pozycyjnym zegarze — pełne okrążenie (16 kroków) plus 2 dodatkowe.

a ≡ b (mod n) jest relacją, a a mod n oblicza resztę modulo. Dla dodatniego n pythonowe a % n == b % n sprawdza kongruencję także przy ujemnych argumentach. W językach, gdzie % daje resztę ze znakiem, np. JavaScript, najpierw znormalizuj oba wyniki: ((a % n) + n) % n. Na przykład -1 i 15 są przystające modulo 16, choć JavaScript zwraca różne surowe reszty.

Dlaczego przepis „zaneguj i dodaj 1” działa

Mamy już potrzebne narzędzia, by zobaczyć, dlaczego przepis z wcześniejszej części faktycznie daje właściwe kodowanie liczb ujemnych.

Centralnym pojęciem jest element przeciwny (additive inverse). Na zegarze modularnym „minus x” to wcale nie znak minus — to ta pozycja zegara, która dodana do x sprowadza Cię z powrotem na pozycję 0. Tę pozycję nazywa się elementem przeciwnym do x i właśnie ją uzupełnienie do dwóch zapisuje w bitach. A przepis, który widzieliśmy wcześniej — weź wersję dodatnią, zaneguj wszystkie bity i dodaj 1 — robi dokładnie to: jest bitową procedurą obliczania elementu przeciwnego.

Znajdźmy element przeciwny do 5 na naszym 16-pozycyjnym zegarze. Chcemy takiej pozycji y, że 5 + y trafia na 0. Startując z 5 i idąc 11 kroków zgodnie z ruchem wskazówek, trafiamy na 16, co zawija się do 0. Zatem y = 11:

5+11=160(mod16)5 + 11 = 16 \equiv 0 \pmod{16}

Element przeciwny 5 na 16-pozycyjnym zegarze to więc 11. A 11 binarnie to 1011 — dokładnie ten wzorzec bitowy, który przepis „zaneguj i dodaj 1” dał wcześniej dla −5.

Możemy teraz uogólnić to, co zrobiliśmy z 5 i 11, na dowolną liczbę bitów. Na zegarze o 2n2^n pozycjach elementem przeciwnym do dowolnej wartości x jest 2nx2^n - x, ponieważ

x+(2nx)=2n0(mod2n)x + (2^n - x) = 2^n \equiv 0 \pmod{2^n}

Zatem uzupełnienie do dwóch przechowuje −x jako wzorzec bitowy liczby 2nx2^n - x czytany bez znaku. To właśnie zaobserwowaliśmy na zegarze, gdzie ujemne wartości ze znakiem −1, −2, …, −8 siedzą na tych samych pozycjach co liczby bez znaku 15, 14, …, 8 — każda jest elementem przeciwnym do swojego dodatniego odpowiednika.

Na koniec: „zaneguj bity, potem dodaj 1” to po prostu szybki, bitowy sposób obliczenia 2nx2^n - x bez wykonywania faktycznego odejmowania:

  • Zanegowanie każdego bitu xx daje 2n1x2^n - 1 - x (ta wartość pośrednia nazywa się uzupełnieniem do jedynki). Dla 4 bitów przy x=5=x = 5 = 0101: negacja daje 1010, co bez znaku czyta się jako 10, i faktycznie 1615=1016 - 1 - 5 = 10.
  • Dodanie 1 daje 2nx2^n - x. Kontynuując przykład: 10+1=1110 + 1 = 11 — element przeciwny, który policzyliśmy powyżej.

Przepis nie jest więc sprytną sztuczką wymyśloną przez projektantów — jest bitowym skrótem do obliczania modularnego elementu przeciwnego.

Cały format w jednym zdaniu

Uzupełnienie do dwóch to arytmetyka modularna bez znaku z innym oznakowaniem pozycji zegara odpowiadających większym wartościom bez znaku. To cały schemat. Każda właściwość — przepis, jeden układ, zawijanie, pojedyncze zero — jest konsekwencją tej jednej idei.

To wyjaśnia również, dlaczego jeden układ sumujący obsługuje arytmetykę i ze znakiem, i bez znaku. Sprzęt nie wie i nie dba o to, czy oznaczasz pozycje na zegarze jako „ze znakiem”, czy „bez znaku”. On po prostu dodaje modulo 2n2^n. Czy zinterpretujesz wzorzec 1101 jako 13 bez znaku, czy jako -3 ze znakiem, procesor traktuje oba argumenty jako pozycje zegara, idzie do przodu o tyle kroków, ile wynosi drugi argument, i trafia na jakąś pozycję. Obie interpretacje wyłaniają się z tego, jak czytasz wynik, a nie z tego, jak procesor go obliczył.

Kod z przesunięciem: schemat IEEE-754

Uzupełnienie do dwóch jest znakomite w tym, do czego zostało zaprojektowane: pozwala wykonywać arytmetykę liczb całkowitych ze znakiem i bez znaku za pomocą tego samego układu. Ale gdy odejdziesz od „chcę dodawać i odejmować liczby całkowite ze znakiem” i zadasz reprezentacji ze znakiem inne pytania, zaczyna być niewygodne. Pytania w rodzaju:

  • Jak porównać dwie wartości ze znakiem bit po bicie, tak samo jak procesor porównuje wartości bez znaku?
  • Gdzie w przestrzeni wzorców bitowych naturalnie wypadną wzorce specjalne (najmniejszy, największy, zero)?
  • Czy kodowanie zachowuje porządek od najmniejszych do największych wartości bez dodatkowej obsługi bitu znaku?

W porządku bez znaku wzorce 0111...1111 (największa wartość dodatnia) i 1000...0000 (najmniejsza wartość ujemna) sąsiadują ze sobą pośrodku zakresu. Przekroczenie tej granicy powoduje skok wartości ze znakiem wstecz. Kodowanie z przesunięciem unika tego skoku, co przydaje się przy porównywaniu wykładników zmiennoprzecinkowych.

Dlatego arytmetyka komputerowa używa innej reprezentacji ze znakiem, gdy te właściwości mają znaczenie: kodu z przesunięciem (offset binary, zwanego też biased binary albo excess-K). To schemat, którego IEEE-754 używa do wykładnika każdej liczby zmiennoprzecinkowej, a dokładnie dlaczego — zobaczymy później. Na razie przyjrzyjmy się, jak on właściwie działa.

Procedura kodowania w kodzie z przesunięciem jest prosta: obliczamy przesunięcie (bias), dodajemy je do liczby, którą chcemy zapisać, a wynikowa wartość jest tym, co faktycznie zostaje zapisane (w razie potrzeby przeliczone na binarne). Aby zademonstrować kroki, zobaczmy, jak liczbę 3 można zapisać na 4 bitach.

Najpierw znajdujemy przesunięcie za pomocą wzoru wspomnianego w standardzie IEEE-754:

K=2n11K = 2^{n-1} - 1

gdzie n to liczba bitów. Przesunięcie dla 4 bitów to więc 7. Następnie dodajemy przesunięcie do liczby pierwotnej: 3 + 7 = 10.

Wynikowa liczba 10 to sposób, w jaki liczba 3 jest przechowywana w schemacie kodu z przesunięciem. Ponieważ do uzyskania wyniku 10 użyliśmy systemu dziesiętnego, musimy przeliczyć go na binarny:

10=25+05=22+12=21+01=20+11010=10102\begin{aligned} 10 &= 2 \cdot 5 + 0 \\ 5 &= 2 \cdot 2 + 1 \\ 2 &= 2 \cdot 1 + 0 \\ 1 &= 2 \cdot 0 + 1 \\ \\ 10_{10} &= 1010_2 \end{aligned}

Jeśli nie znasz algorytmu konwersji albo chcesz wiedzieć, dlaczego działa, zajrzyj do mojego artykułu o algorytmach konwersji dziesiętno-binarnej.

Definiowanie przesunięcia

Załóżmy, że mamy tylko 4 bity na przechowywanie liczb. Każdy bit ma dwie możliwe wartości, co daje nam 24=162^4 = 16 różnych wzorców bitowych. Pytanie brzmi: co te 16 liczb reprezentuje. Załóżmy, że interesuje nas przechowywanie wyłącznie liczb całkowitych nieujemnych. Wtedy zakres to:

[0;15]10[0000;1111]2[0; 15]_{10} \qquad [0000; 1111]_2

Ale jeśli włączymy liczby ujemne, zakres może być różny:

[1;14]10[0000;1111]2[7;8]10[0000;1111]2[8;7]10[0000;1111]2\begin{aligned} [-1; 14]_{10} &\qquad [0000; 1111]_2 \\ [-7; 8]_{10} &\qquad [0000; 1111]_2 \\ [-8; 7]_{10} &\qquad [0000; 1111]_2 \end{aligned}

Ciekawe jest tu to, że choć zmieniamy zakres w zapisie dziesiętnym, w binarnym pozostaje on ten sam — tylko że teraz najmniejsza liczba 0000 reprezentuje liczbę ujemną. To tak, jakby ta najmniejsza liczba była przesunięta w dół od zera o 1 w pierwszym przypadku, o 7 w drugim i o 8 w trzecim.

Innymi słowy, przesunięcie K to po prostu wybór gdzie podzielić ustalony zbiór wzorców bitowych między liczby ujemne i nieujemne — decyduje, ile wartości przypada każdej stronie zera. Nie ma jednego matematycznego standardu, jak je wybierać, jest tylko konwencja. Powszechne są dwie możliwości:

K=2n1(podział roˊwny, zakres [2n1; 2n11])K=2n11(IEEE-754, zakres [(2n11); 2n1])\begin{aligned} K &= 2^{n-1} \quad &\text{(podział równy, zakres } [-2^{n-1};\ 2^{n-1}-1]) \\ K &= 2^{n-1} - 1 \quad &\text{(IEEE-754, zakres } [-(2^{n-1}-1);\ 2^{n-1}]) \end{aligned}

Dla 4 bitów K = 8 daje równo podzielony zakres [-8; 7], natomiast K = 7 (wybór IEEE-754) daje zakres [-7; 8] z jedną dodatkową wartością dodatnią.

Załóżmy, że musimy zapisać liczbę 3 na 4 bitach. Użyjemy wzoru na przesunięcie z IEEE-754 K=2n11K = 2^{n-1} - 1, który dla n=4n = 4 daje K=7K = 7. To przesunięcie dzieli 16 wzorców bitowych na zakres [-7; 8], więc wzorzec 0000 reprezentuje -7, a 1111 reprezentuje 8. Skoro 0000 to -7, jaką liczbę musimy dodać, by dojść do 3? To 10. W tym przykładzie wykorzystujemy wszystkie wzorce; IEEE-754 rezerwuje pola wykładnika złożone z samych zer i samych jedynek, co omówimy niżej.

Zobaczmy, co nam to daje:

00002+10102=10102710+1010=310\begin{aligned} 0000_2 + 1010_2 &= 1010_2 \\ -7_{10} + 10_{10} &= 3_{10} \end{aligned}

Pokazuje to, że liczba 3 jest przechowywana jako 1010 binarnie przy przesunięciu 7. Powinno też być łatwo dostrzec, skąd bierze się operacja dodania przesunięcia w celu uzyskania reprezentacji liczby w kodzie z przesunięciem:

7+10=33+7=10-7 + 10 = 3 \rightarrow 3 + 7 = 10

I w konsekwencji: jeśli dodawaliśmy przesunięcie, by uzyskać reprezentację liczby w kodzie z przesunięciem, to aby przeliczyć liczbę z powrotem, powinniśmy je odjąć.

Zalety wobec uzupełnienia do dwóch

Największą zaletą kodu z przesunięciem nad uzupełnieniem do dwóch jest to, że pozwala porównywać liczby tak, jak są, w porządku leksykograficznym, bez potrzeby dodatkowych operacji. Porównajmy na przykład dwie liczby, 3 i -3, reprezentowane na 4 bitach. W kodzie z przesunięciem mają następującą postać:

310=10102310=01002\begin{aligned} 3_{10} &= 1010_2 \\ -3_{10} &= 0100_2 \end{aligned}

Przy porównywaniu bit po bicie komputer już po pierwszym bicie rozpoznaje, że pierwsza liczba jest większa. Porządku leksykograficznego nie da się zastosować do liczb przechowywanych w uzupełnieniu do dwóch:

310=00112310=11012\begin{aligned} 3_{10} &= 0011_2 \\ -3_{10} &= 1101_2 \end{aligned}

Aby je porównać, komputer musi wykonać dodatkowe operacje.

Monotoniczne uporządkowanie to główna zaleta, a wynika z niej kilka ważnych konsekwencji — i razem wyjaśniają one, dlaczego IEEE-754 wybrał do wykładnika zmiennoprzecinkowego właśnie kod z przesunięciem.

Dla dodatnich skończonych binarnych liczb zmiennoprzecinkowych odczytanie kodu jako liczby bez znaku zachowuje porządek liczbowy. Wartości ujemne i specjalne nadal wymagają dodatkowej obsługi; przesunięte wykładniki nie eliminują całej logiki porównywania liczb zmiennoprzecinkowych.

Schemat ładnie umieszcza też zarezerwowane wzorce bitowe na granicach. Pola wykładnika złożone z samych zer i samych jedynek leżą na dwóch skrajach zakresu, a IEEE-754 używa ich do kodowania wartości specjalnych — ±0 i liczby zdenormalizowane na dole, ±∞ i NaN na górze. Przesunięcie (np. 1023 dla fp64) jest dobrany tak, by użyteczne wykładniki wypadały między tymi zarezerwowanymi końcami, a liczby zdenormalizowane zapewniają stopniowy niedomiar, ponieważ sąsiadują z dokładnym zerem na dolnym skraju.

Wybór nie jest więc arbitralny — kod z przesunięciem to reprezentacja, która łączy wszystkie te właściwości.

Kiedy zakres jest asymetryczny

Jeśli każdy z 2n2^n wzorców bitowych oznacza inną kolejną liczbę całkowitą, a zakres zawiera zero, nie może być symetryczny względem zera: symetria wymaga nieparzystej liczby wartości. Argument zakłada brak zduplikowanych kodów i wzorców zarezerwowanych.

Kod uzupełnień do dwóch ma o jedną wartość ujemną więcej. Kod z przesunięciem pozwala wybrać wiele zakresów przez dobór przesunięcia; tylko dwa omówione niemal wyśrodkowane warianty różnią się o jedną wartość. Znak-moduł ma symetryczny zakres i dwa zera. Jego arytmetyka wymaga innej logiki, ale nie jest matematycznie błędna.

Widać to bezpośrednio w stałych IEEE-754: dla fp64 użyteczny zakres wykładników (po wyłączeniu zarezerwowanych kodów) to od -1022 do +1023, wciąż z przesunięciem o jeden. Dlatego największa skończona liczba podwójnej precyzji wynosi około 1.8×103081.8 \times 10^{308}, a najmniejsza znormalizowana dodatnia około 2.2×103082.2 \times 10^{-308} — wykładniki dziesiętne mają zbliżone, lecz nie równe wartości bezwzględne.

Przegląd tego, jak cztery podstawowe operacje (dodawanie, odejmowanie, mnożenie, dzielenie) faktycznie działają na liczbach całkowitych w uzupełnieniu do dwóch — i co dzieje się na poziomie bitów, przy przepełnieniu lub innych błędach — znajdziesz w artykule Jak działa arytmetyka binarna: liczby całkowite w uzupełnieniu do dwóch i liczby zmiennoprzecinkowe IEEE-754.