โปรแกรมเมอร์ส่วนใหญ่เชื่อว่าการแปลงจำนวนเต็มเป็นจำนวนทศนิยมในคอมพิวเตอร์นั้นปลอดภัยและแม่นยำ ความเชื่อนี้ได้นำไปสู่ข้อผิดพลาดนับไม่ถ้วนและพฤติกรรมที่ไม่คาดคิดในระบบซอฟต์แวร์ ความจริงนั้นซับซ้อนกว่ามากและเผยให้เห็นข้อจำกัดพื้นฐานในวิธีที่คอมพิวเตอร์แทนค่าตัวเลข
ความจริงที่น่าประหลาดใจเกี่ยวกับการแปลงตัวเลข
เมื่อคุณแปลงจำนวนเต็ม 32 บิตเป็น float 32 บิต คุณอาจสูญเสียความแม่นยำที่แน่นอน สิ่งนี้เกิดขึ้นเพราะทั้งสองประเภทข้อมูลใช้พื้นที่จัดเก็บเท่ากัน แต่แทนค่าชุดตัวเลขที่แตกต่างกันโดยสิ้นเชิง มีเพียงประมาณ 3.5% ของจำนวนเต็ม 32 บิตทั้งหมดที่สามารถแทนค่าได้อย่างแม่นยำเป็น float 32 บิต สถานการณ์จะแย่ลงกับตัวเลข 64 บิต ซึ่งมีเพียง 0.5% ของจำนวนเต็มที่สามารถแทนค่าได้อย่างสมบูรณ์แบบเป็น double
ปัญหานี้เกิดจากวิธีการทำงานของจำนวนจุดลอยตัว พวกมันใช้สัญกรณ์วิทยาศาสตร์ แบ่งบิตระหว่างตัวเลขจริงและเลขชี้กำลังที่กำหนดตำแหน่งจุดทศนิยม float 32 บิตใช้ 23 บิตสำหรับเก็บตัวเลขสำคัญ 8 บิตสำหรับเลขชี้กำลัง และ 1 บิตสำหรับเครื่องหมาย ซึ่งหมายความว่าจำนวนเต็มที่ต้องการความแม่นยำมากกว่า 23 บิตจะไม่สามารถใส่ได้อย่างแม่นยำ
สัญกรณ์วิทยาศาสตร์: วิธีการเขียนตัวเลขเป็นสัมประสิทธิ์คูณด้วยกำลังของ 10 (เช่น 1.23 × 10^5 สำหรับ 123,000)
สถิติการแปลงจำนวนเต็ม 32 บิตเป็นทศนิยม:
- จำนวนเต็ม 32 บิตทั้งหมด: 2^32 (ประมาณ 4.3 พันล้าน)
- สามารถแสดงได้อย่างแม่นยำในรูปแบบทศนิยม 32 บิต: 9 × 2^23 (ประมาณ 75 ล้าน)
- เปอร์เซ็นต์ที่สามารถแสดงได้อย่างแม่นยำ: ~3.5%
- บิตความแม่นยำใน float32: 23 บิต
- บิตเลขชี้กำลังใน float32: 8 บิต
ผลกระทบในโลกจริงต่อการพัฒนาซอฟต์แวร์
การสูญเสียความแม่นยำนี้ส่งผลต่อการเขียนโปรแกรมในชีวิตประจำวันในรูปแบบที่ไม่คาดคิด การเขียนโปรแกรมกราฟิก การคำนวณทางการเงิน และระบบการจัดลำดับข้อมูลล้วนพบปัญหาเหล่านี้ รูปแบบข้อมูลบางอย่างแปลงระหว่างจำนวนเต็มและ float โดยอัตโนมัติระหว่างการอัปเกรดเวอร์ชัน ซึ่งอาจทำให้ข้อมูลเสียหายโดยไม่มีการเตือน
ชุมชนได้ระบุปัญหาเชิงปฏิบัติหลายประการ ข้อกำหนดของ WebGPU ต้องคำนึงถึงข้อจำกัดนี้เมื่อออกแบบฟังก์ชันการแปลงของพวกเขา ภาษาโปรแกรมมิ่งหลายภาษาอนุญาตให้แปลงจากจำนวนเต็มเป็น float โดยอัตโนมัติ แต่ปิดกั้นการแปลงในทิศทางตรงกันข้าม ทำให้เกิดความสัมพันธ์ที่ไม่สมมาตรซึ่งทำให้นักพัฒนาสับสน
อีกข้อเท็จจริงที่น่าเสียดายคือ max int (ทั้งแบบมีเครื่องหมายและไม่มีเครื่องหมาย) ก็ไม่ใช่ float เช่นกัน ซึ่งหมายความว่าคุณไม่สามารถเขียนการแปลง ftoi แบบจำกัดขอบเขตได้โดยใช้เฉพาะจุดลอยตัว
ทำไมสิ่งนี้จึงสำคัญสำหรับภาษาโปรแกรมมิ่งที่แตกต่างกัน
ภาษาโปรแกรมมิ่งต่างๆ จัดการปัญหานี้ในรูปแบบที่หลากหลาย JavaScript ใช้เฉพาะ float 64 บิตสำหรับตัวเลขทั้งหมด ซึ่งสามารถแทนค่าจำนวนเต็ม 32 บิตได้อย่างแม่นยำ แต่ยังคงมีข้อจำกัดด้านความแม่นยำสำหรับค่าที่ใหญ่กว่า Python รองรับจำนวนเต็มขนาดไม่จำกัด ทำให้ปัญหาการแปลงชัดเจนยิ่งขึ้นเมื่อตัวเลขขนาดใหญ่เหล่านี้พบกับการดำเนินการจุดลอยตัว
ปัญหาจะซับซ้อนมากขึ้นเมื่อพิจารณาว่านักพัฒนาหลายคนคิดว่า float เป็นเวอร์ชันคอมพิวเตอร์ของจำนวนจริง แทนที่จะเข้าใจธรรมชาติที่แท้จริงของมันในฐานะสัญกรณ์วิทยาศาสตร์ที่มีความแม่นยำจำกัด แบบจำลองทางความคิดนี้นำไปสู่สมมติฐานที่ไม่ถูกต้องเกี่ยวกับเวลาที่การแปลงปลอดภัย
ความแม่นยำ: จำนวนหลักสำคัญที่รูปแบบตัวเลขสามารถเก็บได้อย่างแม่นยำ
โครงสร้างรูปแบบ IEEE 754 Floating Point:
ส่วนประกอบ | Float32 | Float64 |
---|---|---|
Sign bit | 1 bit | 1 bit |
Exponent | 8 bits | 11 bits |
Mantissa/Fraction | 23 bits | 52 bits |
รวม | 32 bits | 64 bits |
รูปแบบ | ±1.f × 2^e | ±1.f × 2^e |
การทำความเข้าใจคณิตศาสตร์เบื้องหลังข้อจำกัด
คำอธิบายทางคณิตศาสตร์เผยให้เห็นว่าทำไมปัญหานี้จึงหลีกเลี่ยงไม่ได้ ทั้งจำนวนเต็ม 32 บิตและ float 32 บิตสามารถแทนค่าจำนวนค่าที่แตกต่างกันได้เท่ากันพอดี อย่างไรก็ตาม พวกมันแทนค่าชุดตัวเลขที่แตกต่างกันโดยสิ้นเชิง Float สามารถแทนค่าตัวเลขที่ใหญ่มากและเล็กมาก แต่มีช่องว่างระหว่างค่าที่แทนได้ จำนวนเต็มแทนค่าจำนวนเต็มที่ต่อเนื่องกันภายในช่วงที่จำกัด
สิ่งนี้สร้างการแลกเปลี่ยนพื้นฐาน เมื่อคุณแปลงจากจำนวนเต็มเป็น float คุณได้รับความสามารถในการแทนค่าเศษส่วนและตัวเลขที่ใหญ่กว่ามาก แต่คุณสูญเสียการรับประกันว่าจำนวนเต็มทุกตัวในช่วงของคุณสามารถแทนค่าได้อย่างแม่นยำ การแปลงนำข้อผิดพลาดการปัดเศษที่สามารถสะสมในการคำนวณและทำให้เกิดข้อผิดพลาดที่ละเอียดอ่อน
การอภิปรายของชุมชนเผยให้เห็นว่าโปรแกรมเมอร์ที่มีประสบการณ์หลายคนได้พบปัญหานี้โดยไม่คาดคิด โดยเฉพาะอย่างยิ่งเมื่อทำงานกับชุดข้อมูลขนาดใหญ่หรือการคำนวณที่แม่นยำซึ่งทุกบิตมีความสำคัญ
อ้างอิง: Most ints are not floats