Μετάβαση στο περιεχόμενο
Topos Uranos

Περίληψη

Οι αριθμοί με δεκαδικά ενός προγράμματος δεν είναι οι πραγματικοί αριθμοί των μαθηματικών: είναι ένα πεπερασμένο υποσύνολο ρητών με παρονομαστή δύναμη του δύο, και κάθε τιμή που δεν ανήκει σε αυτό το υποσύνολο αντικαθίσταται από την πλησιέστερη που ανήκει. Το μάθημα αυτό μελετά πώς αποθηκεύεται ένας αριθμός κινητής υποδιαστολής σε 64 bit (πρόσημο, εκθέτης και mantissa), γιατί το 0.10.1 δεν έχει ακριβή αναπαράσταση και ποια τιμή παίρνει τη θέση του, πόσα ψηφία είναι ασφαλή στον float και στον double, πώς συσσωρεύονται τα σφάλματα στρογγυλοποίησης, γιατί δύο πραγματικοί δεν συγκρίνονται με == και πώς συγκρίνονται με μια αιτιολογημένη ανοχή, και τι είναι το άπειρο και ο μη αριθμός, που η μορφή δεσμεύει για τα αποτελέσματα που δεν χωρούν ή δεν υπάρχουν.

Μαθησιακοί στόχοι

  1. Να εξηγείτε γιατί το 0.10.1 δεν έχει ακριβή δυαδική αναπαράσταση και ποια τιμή αποθηκεύεται στη θέση του.
  2. Να προβλέπετε την επίδραση της στρογγυλοποίησης σε επαναλαμβανόμενες προσθέσεις και στην πρόσθεση αριθμών πολύ διαφορετικών μεγεθών.
  3. Να συγκρίνετε πραγματικούς αριθμούς με κατάλληλη ανοχή αντί για ==, αιτιολογώντας την επιλογή της ανοχής.
  4. Να επιλέγετε ανάμεσα σε float και double ανάλογα με την απαιτούμενη ακρίβεια, και να αναγνωρίζετε το άπειρο και τον μη αριθμό.

Επιστημονική γραφή στη βάση 2

Το μάθημα Οι ακέραιοι και η αναπαράστασή τους έδειξε ότι ένας ακέραιος nn bit αναπαριστά ακριβώς 2n2^n διαδοχικές τιμές. Ωστόσο, οι αριθμοί με δεκαδικά θέτουν μια διαφορετική δυσκολία: ανάμεσα σε δύο οποιουσδήποτε πραγματικούς υπάρχουν άπειροι άλλοι, και κανένα πεπερασμένο πλήθος bit δεν μπορεί να τους διακρίνει όλους. Η λύση που υιοθετούν όλοι οι σημερινοί υπολογιστές είναι η ίδια που χρησιμοποιεί η επιστήμη για να γράφει πολύ μεγάλα ή πολύ μικρά μεγέθη: η επιστημονική γραφή. Στη βάση 10, η μέση απόσταση της Γης από τον Ήλιο γράφεται 1.496×10111.496 \times 10^{11} μέτρα· ο αριθμός έχει ένα πρόσημο, λίγα σημαντικά ψηφία (τη mantissa, 1.4961.496) και μια δύναμη της βάσης (τον εκθέτη, 1111), που λέει πού μπαίνει η υποδιαστολή. Αν αυτό ισχύει, με σταθερό πλήθος ψηφίων καλύπτονται μεγέθη πολύ διαφορετικών τάξεων, με τίμημα να φυλάσσονται μόνο τα πρώτα ψηφία του καθενός.

Ο υπολογιστής κάνει το ίδιο στη βάση 2. Κάθε αριθμός διάφορος του μηδενός γράφεται με μοναδικό τρόπο ως

x=(−1)s×1.m1m2m3…m52 (2)×2ex = (-1)^{s} \times 1.m_1 m_2 m_3 \ldots m_{52} \, {}_{(2)} \times 2^{e}

όπου ss είναι το πρόσημο (0 για θετικό, 1 για αρνητικό), 1.m1m2…1.m_1 m_2 \ldots είναι η mantissa γραμμένη στο δυαδικό, της οποίας το πρώτο ψηφίο είναι πάντα 1, και ee είναι ο εκθέτης. Το ότι το πρώτο ψηφίο είναι πάντα 1 έχει μια πρακτική συνέπεια: δεν χρειάζεται να αποθηκευτεί, και το bit που εξοικονομείται κερδίζεται για την ακρίβεια. Το όνομα κινητή υποδιαστολή περιγράφει ακριβώς αυτή τη μορφή: η υποδιαστολή δεν έχει σταθερή θέση ανάμεσα στα ψηφία, αλλά «μετακινείται» ανάλογα με τον εκθέτη.

Ο τύπος double της C++ χρησιμοποιεί σε όλους τους σημερινούς μεταγλωττιστές τη διεθνώς τυποποιημένη δυαδική μορφή διπλής ακρίβειας, των 64 bit: 1 bit για το πρόσημο, 11 bit για τον εκθέτη και 52 bit για τη mantissa, χωρίς το αρχικό ψηφίο. Ο εκθέτης αποθηκεύεται μετατοπισμένος: το πεδίο των 11 bit περιέχει e+1023e + 1023, ώστε φυλάσσει μη προσημασμένες τιμές από 1 έως 2046 για εκθέτες από −1022-1022 έως 10231023 (οι τιμές 0 και 2047 του πεδίου δεσμεύονται για το μηδέν, τους πολύ μικρούς αριθμούς, το άπειρο και τον μη αριθμό, που θα δούμε στο τέλος του μαθήματος). Ο τύπος float χρησιμοποιεί τη μορφή απλής ακρίβειας, των 32 bit: 1 για το πρόσημο, 8 για τον εκθέτη και 23 για τη mantissa.

Τα bit ενός double μπορούν να φανούν. Το πρόγραμμα που ακολουθεί χρησιμοποιεί τη std::bit_cast, από την κεφαλίδα <bit>, που ερμηνεύει ξανά τα 64 bit του αριθμού ως μη προσημασμένο ακέραιο 64 bit, χωρίς να αλλάξει κανένα· εδώ χρησιμοποιείται χωρίς περαιτέρω εξήγηση, ως όργανο για να κοιτάξουμε τη μνήμη. Η προδιαγραφή μορφής {:064b} γράφει αυτόν τον ακέραιο στο δυαδικό με 64 ψηφία, όπως στο μάθημα που προηγήθηκε, και η {:016X} στο δεκαεξαδικό με 16.

Πρόγραμμα σε C++
#include <bit>#include <cstdint>#include <print>​int main(){    const double tenth{0.1};    // Τα 64 bit του αριθμού, διαβασμένα ως μη προσημασμένος ακέραιος.    const std::uint64_t bits{std::bit_cast<std::uint64_t>(tenth)};    std::println("{:064b}", bits);    std::println("{:016X}", bits);}
Έξοδος
00111111101110011001100110011001100110011001100110011001100110103FB999999999999A
ΠαράδειγμαΤα 64 bit του 0.1

Το σχήμα ομαδοποιεί τα 64 bit που έγραψε το πρόγραμμα και τα διαβάζει πεδίο προς πεδίο.

Απόδειξη

  1. 1+11+52=641 + 11 + 52 = 64

    Τα 64 bit, στο δεκαεξαδικό 3FB999999999999A. Διαβάζονται από αριστερά προς τα δεξιά: 1 bit για το πρόσημο, 11 για τον εκθέτη και 52 για τη mantissa.

  2. s=0s = 0

    Το πρόσημο είναι 0: ο αριθμός είναι θετικός.

  3. e=1019−1023=−4e = 1019 - 1023 = -4

    Το πεδίο του εκθέτη, 0111111101101111111011 στο δυαδικό, ισούται με 1019· αφαιρείται η μετατόπιση, 1023, και ο εκθέτης είναι −4-4.

  4. m≈1.6000000000000000888m \approx 1.6000000000000000888

    Η mantissa, με το αρχικό 1 που δεν αποθηκεύεται, είναι 1.1001100110011…10101.1001100110011\ldots1010 στο δυαδικό: η ομάδα 10011001 επαναλαμβάνεται και η τελευταία ομάδα, 10101010, είναι στρογγυλοποιημένη προς τα πάνω. Η τιμή της είναι περίπου 1.60000000000000008881.6000000000000000888.

  5. x=0.1000000000000000055511…≠0.1x = 0.1000000000000000055511\ldots \neq 0.1

    Ο αριθμός που αποθηκεύτηκε είναι +1.6000000000000000888×2−4+1.6000000000000000888 \times 2^{-4}, δηλαδή 0.1000000000000000055511…0.1000000000000000055511\ldots: πολύ κοντά στο 0.10.1, αλλά όχι 0.10.1.