Format liczb zmiennoprzecinkowych IEEE 754
Dokumentacja procesorów x64 [1] wyróżnia dwa podstawowe typy danych dla liczb zmiennoprzecinkowych:
- pojedynczej precyzji (ang. single precision) o rozmiarze 32 bitów,
- podwójnej precyzji (ang. double precision) o rozmiarze 64 bitów.
Sposób reprezentacji liczb zmiennoprzecinkowych w rejestrach procesora i pamięci urządzenia opartego o architekturę x64 definiuje standard IEEE 754. [5]
Poszczególne komponenty liczby zmiennoprzecinkowej to bit znaku (ang. sign), wykładnik z przesunięciem (ang. biased exponent) oraz część znacząca (ang. significand).
Wstęp dokumentu standardu IEEE 754 [5] zawiera następujące definicje tych elementów:
- Znak (ang. sign) — bit znaku, który przyjmuje wartość 1 dla liczb ujemnych lub 0 dla liczb dodatnich.
- Wykładnik z przesunięciem (ang. biased exponent) — suma wykładnika i stałej, nazywanej przesunięciem (ang. bias), która sprawia, że rezultat nie jest liczbą ujemną.
- Część znacząca (ang. significand) — komponent skończonej liczby zmiennoprzecinkowej, który zawiera jej znaczące cyfry. Może być liczbą całkowitą, ułamkiem lub formą stałoprzecinkową.
Poniżej przedstawiono obliczenie wartości przykładowej liczby we wspomnianym formacie.
Przykładowa liczba zmiennoprzecinkowa to 0x419051EC w systemie szesnastkowym (heksadecymalnym). Jej rozmiar to 4 bajty (32 bity). Binarnie jest to ciąg bitów 0 10000011 00100000101000111101100.
Wykładnik zawiera przesunięcie (ang. bias) i wynosi 10000011 binarnie, czyli 131 dziesiętnie. Dla jasności przesunięcie (ang. bias) można obliczyć jako 2k-1 - 1, gdzie k to liczba bitów wykładnika, czyli 28-1 - 1 = 27 - 1 = 128 - 1 = 127.
Ogólny wzór dla obliczenia wartości zmiennoprzecinkowej jest następujący (-1)S × 2E - bias × T.
Bit znaku w tym przypadku wynosi zero, czyli (-1)S = (-1)0 = 1.
Wykładnik z przesunięciem wynosi 131, czyli 2E - bias = 2131 - 127 = 24 = 16, ponieważ E = e + bias.
Do obliczenia pozostaje jeszcze część znacząca (ang. significand). Zgodnie ze standardem IEEE 754 [5] na początku wstawiana jest niejawnie (ang. implicit) wartość 1, a następnie poszczególne bity są mnożone przez ujemne potęgi dwójki, czyli w skrócie (1 + 0 × 2-1 + 0 × 2-2 + 1 × 2-3 + ... + 1 × 2-21 + 0 × 2-22 + 0 × 2-23).
Ostateczne obliczenia według głównego wzoru są następujące
(-1)S × 2E - bias × T
= 1.0 × 16.0 × 1.127500057220458984375... ≈ 18.04
Warto zaznaczyć, że w przypadku obliczeń wykonywanych przez program komputerowy występuje tutaj zaokrąglenie części ułamkowej, wynikające ze skończonej precyzji typu zmiennoprzecinkowego o rozmiarze 32 bitów.
Powyższe obliczenia można łatwo zweryfikować następującym poleceniem PowerShell.
[BitConverter]::UInt32BitsToSingle(0x419051EC);
Za pomocą metody UInt32BitsToSingle z klasy System.BitConverter platformy .NET możliwe jest konwertowanie surowych bitów liczby całkowitej bez znaku (32 bity) na typ danych float (klasa Single, pojedyncza precyzja).
Wartość typu numerycznego niebędąca liczbą (ang. Not a Number)
Standard IEEE 754 [5] definiuje NaN jako symboliczne dane typu zmiennoprzecinkowego, które nie są liczbą.
Wyróżnia też dwa rodzaje wartości NaN:
- Ciche NaN (ang. quiet) — operacje, w których wystąpi NaN, propagują tę wartość, nie zgłaszając wyjątku.
- Sygnalizujące NaN (ang. signaling) — pojawienie się NaN jako operandu powoduje zgłoszenie wyjątku.
Warto pamiętać, że sygnalizujący NaN może zostać w rezultacie zamieniony na cichy NaN, ponieważ wyjątek Invalid Operation jest domyślnie maskowany (wyłączony) w urządzeniach opartych o architekturę x64. Dzięki temu nieprawidłowe operacje, takie jak np. dzielenie zero przez zero, nie powodują awarii przerywających działanie programów. [1]
Poniżej przedstawiono dzielenie zero przez zero za pomocą rozkazu vdivpd w Asemblerze MASM x64.
.data
mxcsr_1 dword 0
.code
Main proc
vstmxcsr mxcsr_1
vxorpd ymm3, ymm3, ymm3
vxorpd ymm2, ymm2, ymm2
;0.00 / 0.00 = NaN
vdivpd ymm1, ymm2, ymm3
vldmxcsr mxcsr_1
ret
Main endp
end
W sekcji z danymi (.data) zdefiniowano zmienną mxcsr_1 używaną do przechowywania zawartości rejestru MXCSR. Zawiera on flagi kontrolne i jest rejestrem nieulotnym. [8]
Z tego powodu na początku jego stan jest zachowywany za pomocą rozkazu vstmxcsr w pamięci nazwanej mxcsr_1. Później przed wyjściem z funkcji głównej zachowana wartość rejestru MXCSR jest przywracana instrukcją vldmxcsr mxcsr_1.
Instrukcje vxorpd ymm3, ymm3, ymm3 oraz vxorpd ymm2, ymm2, ymm2 powodują wyzerowanie rejestrów ymm3 i ymm2 poprzez wykonanie alternatywy wykluczającej (ang. eXclusive OR). Rejestry są zerowane, ponieważ operacja alternatywy wykluczającej wykonana na takich samych wartościach zwraca w wyniku zero.
Rozkaz vdivpd ymm1, ymm2, ymm3 wykonuje dzielenie wektorów liczb zmiennoprzecinkowych podwójnej precyzji (64 bity). Oznacza to, że wartości w rejestrze ymm2 są dzielone przez wartości z rejestru ymm3. Wcześniej te rejestry zostały wyzerowane, więc do rejestru ymm1 jako wynik trafia wartość NaN.
Jak już wcześniej wspomniano, wyjątek Invalid Operation jest domyślnie maskowany (wyłączony). W celu włączenia zgłaszania tego wyjątku należy wyzerować bit siódmy (licząc od zera) w rejestrze MXCSR. [1]
Można to osiągnąć rozkazem and mxcsr_2, NOT 00080h wykonującym koniunkcję logiczną (ang. AND) zawartości rejestru MXCSR z maską bitową, której wszystkie bity oprócz siódmego są ustawione na jeden. Operacja tego rodzaju powoduje wyzerowanie siódmego bitu nie zmieniając wartości pozostałych. Należy pamiętać, że wyłączone maskowanie wymaga odpowiedniej obsługi zgłaszanych wyjątków.
.data
mxcsr_1 dword 0
mxcsr_2 dword 0
.code
Main proc
vstmxcsr mxcsr_1
vstmxcsr mxcsr_2
and mxcsr_2, NOT 00080h
vldmxcsr mxcsr_2
vxorpd ymm3, ymm3, ymm3
vxorpd ymm2, ymm2, ymm2
;0.00 / 0.00 = NaN
vdivpd ymm1, ymm2, ymm3
vldmxcsr mxcsr_1
ret
Main endp
end
Inne operacje zwracające NaN to m.in.:
- nieskończoność odjąć nieskończoność (∞ - ∞),
- nieskończoność pomnożona przez zero (∞ × 0.00),
- nieskończoność podzielona przez nieskończoność (∞ ÷ ∞),
- zero podzielone przez zero (0.00 ÷ 0.00, wspomniane wcześniej),
- pierwiastek kwadratowy z liczby ujemnej,
- logarytm z liczby ujemnej,
- arcsin i arccos spoza przedziału [-1..1],
- zwykłe operacje arytmetyczne z udziałem NaN.
Poniżej przedstawiono przykładowe obliczenia w PowerShell.
Wartość NaN wpływa na obliczenia, w których się znajduje, powodując zwracanie wartości niebędącej liczbą. Nazywane jest to terminem, że NaN jest propagowany na inne operacje arytmetyczne z jego udziałem. [5]
Nieskończoność
Inną nietypową wartością, o której warto wspomnieć, jest nieskończoność. Symbol nieskończoności to ∞. Wartość tę należy rozumieć tak, że każda skończona liczba (ang. finite number) jest większa od minus nieskończoności (-∞) i mniejsza od plus nieskończoności (+∞). [5]
Przykładowe operacje zwracające nieskończoność (±∞) to m.in.:
- nieskończoność dodać liczba skończona (∞ + a),
- nieskończoność odjąć liczba skończona (∞ - a),
- nieskończoność pomnożona przez liczbę skończoną (∞ × a),
- nieskończoność podzielona przez liczbę skończoną (∞ ÷ a),
- pierwiastek kwadratowy z nieskończoności (sqrt(∞)),
- liczba skończona podzielona przez zero (a ÷ 0.00),
- logarytm z liczby zero (log(0)),
- funkcja wykładnicza z nieskończoności (exp(∞)).
Nieskończoność może też zostać zwrócona jako rezultat zaokrąglenia wartości, która przekroczyła maksymalny dopuszczalny zakres. Przykładem może być pomnożenie [Double]::MaxValue przez dwa czy też podzielenie [Double]::MaxValue przez małą liczbę np. 0.0014.
Wykrywanie nieprawidłowości za pomocą metody IsNaN
Wartość NaN nie jest równa żadnej innej wartości, a nawet samej sobie. W celu sprawdzenia, czy rezultat to NaN, korzysta się ze specjalnych metod IsNaN.
Unikanie podatności lub łagodzenie ich efektów
Należy pamiętać, że nieskończoności, NaN oraz liczby zdenormalizowane są możliwe i należy wziąć je pod uwagę w obliczeniach zmiennoprzecinkowych. [7]
Liczby zmiennoprzecinkowe są jedynie przybliżeniem ich bieżącej wartości. Nieprecyzyjne wartości mogą być akumulowane podczas obliczeń i dawać nieoczekiwane rezultaty. Należy też uważać przy porównywaniu liczb zmiennoprzecinkowych między sobą lub sprawdzania czy ich wartość to zero. [7]
Wykaz literatury
[1] Advanced Micro Devices, Inc. (AMD), AMD64 Architecture Programmer's Manual, 2024.
[2] Arm Limited, Arm Architecture Reference Manual for A-profile architecture, 2025.
[3] Ecma International, Common Language Infrastructure (CLI), 2012.
[4] Ecma International, C# Language Specification, 2023.
[5] Institute of Electrical and Electronics Engineers, IEEE Standard for Floating-Point Arithmetic, 2019.
[6] Intel Corporation, The Intel 64 and IA-32 Architectures Software Developer's Manual, 2025.
[7] ISO/IEC, Programming languages. Avoiding vulnerabilities in programming languages, 2024.
[8] Microsoft Corporation, x64 Calling Convention, 2026.