ลักษณะที่ไม่จริงของประเภทจริง หรือระวังเรื่อง 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 ให้เป็นจริง:

********* ЗАПРОС *********
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) เราจะได้อะไร? ในบรรทัดบนสุด ฉันจะให้ "mantissa" สำหรับประเภท 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 และ PRECISION สองเท่า เราก็จะได้คำตอบของปริศนานี้:

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

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

นอกจากนี้ยังอธิบายตัวอย่างที่สามข้างต้นด้วย หมายเลข 123 นั้นง่าย มันเป็นไปไม่ได้ที่จะใส่แมนทิสซาลงใน 24 บิต (23 ชัดเจน + 1 โดยนัย) จำนวนเต็มสูงสุดที่สามารถใส่ลงใน 24 บิตได้คือ 224-1 = 16 ดังนั้น หมายเลข 777 ของเราจึงถูกปัดเศษเป็น 215 ที่เป็นตัวแทนที่ใกล้ที่สุด โดยการเปลี่ยนประเภทเป็น DOUBLE PRECISION เราจะไม่เห็นสถานการณ์นี้อีกต่อไป:

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

นั่นคือทั้งหมดที่ ปรากฎว่าไม่มีปาฏิหาริย์ แต่ทุกสิ่งที่อธิบายไว้เป็นเหตุผลที่ดีที่จะพิจารณาว่าคุณต้องการประเภท REAL มากเพียงใด บางทีข้อได้เปรียบที่ใหญ่ที่สุดของการใช้งานคือความเร็วในการคำนวณโดยที่ทราบการสูญเสียความแม่นยำ แต่นี่จะเป็นสถานการณ์สากลที่จะพิสูจน์ให้เห็นถึงการใช้ประเภทนี้บ่อยครั้งหรือไม่ อย่าคิดนะ.

ที่มา: will.com

เพิ่มความคิดเห็น