תכונות לא אמיתיות של סוגים אמיתיים, או היזהר עם REAL

לאחר הפרסום מאמרים לגבי תכונות ההקלדה ב-PostgreSQL, ההערה הראשונה הייתה לגבי הקשיים בעבודה עם מספרים אמיתיים. החלטתי להעיף מבט מהיר בקוד של שאילתות SQL הזמינות לי כדי לראות באיזו תדירות הם משתמשים בסוג REAL. מסתבר שמשתמשים בו לעתים קרובות למדי, ומפתחים לא תמיד מבינים את הסכנות שמאחוריו. וזאת למרות שיש לא מעט מאמרים טובים באינטרנט וב-Habré על התכונות של אחסון מספרים אמיתיים בזיכרון המחשב ועל העבודה איתם. לכן, במאמר זה אנסה ליישם תכונות כאלה על PostgreSQL, ואנסה להסתכל במהירות על הצרות הקשורות בהן, כדי שיהיה קל יותר למפתחי שאילתות SQL להימנע מהן.

התיעוד של PostgreSQL קובע בתמציתיות: "ניהול שגיאות כאלה והפצתן במהלך החישוב הוא נושא לענף שלם של מתמטיקה ומדעי המחשב, ואינו מכוסה כאן" (תוך הפניה נבונה של הקורא לתקן IEEE 754). לאיזה סוג של שגיאות הכוונה כאן? בואו נדון בהם לפי הסדר, ובקרוב יתברר מדוע לקחתי שוב את העט.

ניקח לדוגמא בקשה פשוטה:

********* ЗАПРОС *********
SELECT 0.1::REAL;
**************************
float4
--------
    0.1
(1 строка)

כתוצאה מכך, לא נראה שום דבר מיוחד - נקבל את ה-0.1 הצפוי. אבל עכשיו נשווה את זה ל-0.1:

********* ЗАПРОС *********
SELECT 0.1::REAL = 0.1;
**************************
?column?
----------
f
(1 строка)

לא שווה! איזה ניסים! אבל הלאה, יותר. מישהו יגיד, אני יודע ש-REAL מתנהג רע עם שברים, אז אני אכניס שם מספרים שלמים, והכל יהיה בסדר איתם. אוקיי, בואו נעביר את המספר 123 ל-REAL:

********* ЗАПРОС *********
SELECT 123456789::REAL::INT;
**************************
   int4   
-----------
123456792
(1 строка)

והתברר שזה עוד 3! זהו, המאגר שכח סוף סוף איך לספור! או שאנחנו לא מבינים משהו? בואו נבין את זה.

ראשית, בואו נזכור את החומר. כידוע, ניתן להרחיב כל מספר עשרוני בחזקות עשר. אז, המספר 123.456 יהיה שווה ל-1*102 + 2*101 + 3*100 + 4*10-1 + 5*10-2 + ​​6*10-3. אבל המחשב פועל עם מספרים בצורה בינארית, ולכן הם צריכים להיות מיוצגים בצורה של התרחבות בחזקות שתיים. לכן, המספר 5.625 בבינארי מיוצג כ-101.101 ויהיה שווה ל-1*22 + 0*21 + 1*20 + 1*2-1 + 0*2-2 + 1*2-3. ואם חזקות חיוביות של שניים תמיד נותנות מספרים עשרוניים שלמים (1, 2, 4, 8, 16 וכו'), אז עם שליליים הכל יותר מסובך (0.5, 0.25, 0.125, 0,0625 וכו'). הבעיה היא ש לא כל עשרוני יכול להיות מיוצג כשבר בינארי סופי. לפיכך, 0.1 הידוע לשמצה שלנו בצורה של שבר בינארי מופיע כערך המחזורי 0.0(0011). כתוצאה מכך, הערך הסופי של מספר זה בזיכרון המחשב ישתנה בהתאם לעומק הסיביות.

עכשיו זה הזמן לזכור כיצד מספרים אמיתיים מאוחסנים בזיכרון המחשב. באופן כללי, מספר ממשי מורכב משלושה חלקים עיקריים - סימן, מנטיס ומעריך. הסימן יכול להיות פלוס או מינוס, אז מוקצה ביט אחד עבורו. אבל מספר הסיביות של המנטיסה והמעריך נקבע לפי הסוג האמיתי. אז, עבור הסוג REAL, אורך המנטיסה הוא 23 סיביות (ביט אחד שווה ל-1 מתווסף באופן מרומז לתחילת המנטיסה, והתוצאה היא 24), והמעריך הוא 8 סיביות. הסכום הכולל הוא 32 ביטים, או 4 בתים. ולסוג DOUBLE PRECISION, אורך המנטיסה יהיה 52 סיביות, והמעריך יהיה 11 סיביות, בסך הכל 64 סיביות, או 8 בתים. PostgreSQL אינו תומך בדיוק גבוה יותר עבור מספרי נקודה צפה.

בואו נארוז את המספר העשרוני שלנו 0.1 לסוגי REAL ו-DOUBLE PRECISION כאחד. מכיוון שהסימן והערך של המעריך זהים, נתמקד במנטיסה (אני משמיט בכוונה את התכונות הלא ברורות של אחסון ערכי המעריך ואפס ערכים ממשיים, מכיוון שהם מסבכים את ההבנה ומסיחים את הדעת מהמהות של הבעיה, אם מעוניין, עיין בתקן IEEE 754). מה נקבל? בשורה העליונה אתן את ה"מנטיסה" לסוג REAL (בהתחשב בעיגול הביט האחרון ב-1 למספר המיוצג הקרוב ביותר, אחרת הוא יהיה 0.099999...), ובשורה התחתונה - עבור סוג הדיוק הכפול:

0.000110011001100110011001101
0.00011001100110011001100110011001100110011001100110011001

ברור שמדובר בשני מספרים שונים לחלוטין! לכן, בהשוואה, המספר הראשון יהיה מרופד באפסים, ולכן יהיה גדול מהשני (בהתחשב בעיגול - זה המסומן בהדגשה). זה מסביר את העמימות מהדוגמאות שלנו. בדוגמה השנייה, המספר שצוין במפורש 0.1 יצוק לסוג DOUBLE PRECISION, ולאחר מכן מושווה למספר מסוג REAL. שניהם מצטמצמים לאותו סוג, ויש לנו בדיוק מה שאנחנו רואים למעלה. בוא נשנה את השאילתה כך שהכל יבוא על מקומו:

********* ЗАПРОС *********
SELECT 0.1::REAL > 0.1::DOUBLE PRECISION;
**************************
?column?
----------
t
(1 строка)

ואכן, על ידי ביצוע הפחתה כפולה של המספר 0.1 ל-REAL ו-DOUBLE PRECISION, אנו מקבלים את התשובה לחידה:

********* ЗАПРОС *********
SELECT 0.1::REAL::DOUBLE PRECISION;
**************************

      float8       
-------------------
0.100000001490116
(1 строка)

זה גם מסביר את הדוגמה השלישית לעיל. המספר 123 הוא פשוט אי אפשר להתאים את המנטיסה ל-24 ביטים (23 מפורשים + 1 משתמע). המספר השלם המקסימלי שיכול להתאים ל-24 סיביות הוא 224-1 = 16. לכן, המספר שלנו 777 מעוגל ל-215 האפשרי הקרוב ביותר. על ידי שינוי הסוג לדיוק כפול, איננו רואים עוד תרחיש זה:

********* ЗАПРОС *********
SELECT 123456789::DOUBLE PRECISION::INT;
**************************
   int4   
-----------
123456789
(1 строка)

זה הכל. מסתבר שאין ניסים. אבל כל מה שתואר הוא סיבה טובה לחשוב כמה אתה באמת צריך את הסוג האמיתי. אולי היתרון הגדול ביותר של השימוש בו הוא מהירות החישובים עם אובדן דיוק ידוע. אבל האם זה יהיה תרחיש אוניברסלי שיצדיק שימוש כה תכוף מסוג זה? אל תחשוב.

מקור: www.habr.com

הוספת תגובה