29/08/2024
หนึ่งในปัญหาคลาสิก เวลาเขียนโปรแกรมที่ทุกคนต้องเจอเลย
ก็คือการบวกลบเลขทศนิยมในภาษาโปรแกรมของบางภาษา นี้แหละ
เช่น JavaScript, Python, Perl, C #, C, C++, Java, PHP, Fortran
(และอื่นๆ อีกหลายภาษาที่ไม่ได้กล่าวถึง)
หลายครั้งที่มันอาจเพี้ยนได้ เช่น
👉 0.1 บวกกัน 10 ครั้ง ก็ไม่ได้เป็น 1
แต่ได้เป็น 0.9999999999999999
ก็เหมือนกับความรัก บอกรักเธอทุกๆ วัน
แต่ก็ไม่ได้เป็นที่หนึ่งในใจเธอ ...เอ๊ะเกี่ยวกันเปล่า
👉 หรือ 0.1+0.2 ไม่ได้เท่ากับ 0.3
แต่ได้เป็นเศษเสี้ยวในใจเธอ ก็คือ
0.30000000000000004
เอาเป็นว่าคนเขียนโปรแกรมเจอแบบนี้เข้าไป
ก็เหมือนมวยโดนหมัดน๊อคมึนงงในดงโค้ด
:
แต่ใช่ว่ามันจะเพี้ยนทุกครั้ง ซะเมื่อไร เช่น
0.5+0.5 = 1 (ถูกต้องเป๊ะ)
0.2+0.3 = 0.5 (บังเอิญไม่เพี้ยน)
สำหรับ กรณี 0.2 กับ 0.3 มันถูกตัดเศษเหลือเป็น
0.2000000000000000111022302462515654042363166809082031250
กับ
0.2999999999999999888977697537484345957636833190917968750
พอบวกกันจึงได้ 0.5 พอดี แบบฟลุ๊คๆ
(ตรวจสอบดูได้ 0.2+0.3 == 0.5 ได้ค่าออกมาเป็น true)
:
สาเหตุที่เป็นเช่นนี้
ก็เพราะว่าคอมพิวเตอร์มันรู้จักแต่เลขฐาน2 อะนะ
ต่อให้เราเขียนโค้ดใช้เลขฐาน10 ก็ตาม
สุดท้ายถูกแปลงร่างคืนมาหอนเป็นเลขฐาน 2 อยู่ดี
😨 แล้วก็เป็นความซวยที่จะมาเยือนคนเขียนโปรแกรม
เพราะเวลาแปลงเลขฐาน10 ไปเป็นเลขฐาน 2
บางกรณีอย่าง 0.1, 0.2, 0.3 ในระบบเลขฐานสิบ
ต่อให้ฟ้าถล่มดินทลาย มันก็เป็นทศนิยมที่รู้จับ
แต่พอกลายร่างเป็นเลขฐาน เท่านั้นแหละ
อนิจจาวะตะสังขารา เธอเปี้ยนไป
ดันได้เลขทศนิยมไม่รู้จบเสียด้วยซิ
จึงทำให้การเก็บทศนิยมผิดเพี้ยนไปได้
(0.5 แปลงเป็นเลขฐานสองคือ 0.1 ซึ่งเป็นทศนิยมรู้จบ)
สำหรับรูปแบบการจัดเก็บเลขทศนิยมในหลายภาษา
เขาจะนิยมใช้มาตรฐาน IEEE-754 floating point
ซึ่งมีทั้งแบบ single precision (32 bit)
กับ double precision (64 bit)
แล้วแต่ภาษาจะเลือกความละเอียดแบบใด
ถ้าไปดูเงื่อนไขที่เขากำหนด
single precision -> Mantissa เก็บ 23 บิต
double precision -> Mantissa เก็บ 52 บิต
ขอไม่ลงลึกเรื่อง IEEE-754 แล้วกันเนอะ (ตัดจบดื้อๆ)
แต่จะขอยกตัวอย่าง 0.1 จะถูกมองว่าคือ 1/10 ในระบบเลขฐานสิบ
เมื่อเแปลงเป็นเลขทศนิยมฐานสอง จะได้เป็น
0.0001100110011001100110011001100110011001100110011...
เป็นเลขทศนิยมไม่รู้จบในระบบเลขฐานสอง
หรือจะเขียนในรูป Mantissa กับ exponent เท่ห์ๆ
0.1 = 1.10011001100110... x 2^-4
สุดท้าย 0.1 ก็จะเก็บตาม IEEE-754 เช่น เก็บแบบ double precision (64 bit) ก็จะได้เป็น
0011111110111001100110011001100110011001100110011001100110011010...นี้คือสิ่งที่คอมมองเห็นเลข 0.1 ไม่เหมือนคน
ถ้ามันบอกได้ คงบอกว่าเลขทศนิยมฐานสองไม่รู้จบ
เราเก็บจริงนะพวกมานุดโลก
แต่เราไม่ได้เก็บหมดครบทุกบิตอะนะ ไม่รู้อะดิ อิๆๆๆ
เวลาเกิดกรณีเลขฐานสองไม่รู้จบ ค่า Mantissa ต้องถูกปัดเศษตามข้อกำหนด IEEE-754
ถ้าเป็นแบบ single precision ก็ปัดให้เหลือ 23 บิต
หากเป็นแบบ double precision ก็ปัดให้เหลือ 52 บิต
Mantissa เก็บเป็นค่าประมาณการเท่านั้นแหละลูกพี่
เลขเลยเพี้ยนนิดหน่อย อย่าว่ากันนะ ...คอมเอ่ยปากเบาๆ
พอคอมแปลงกลับมาเป็นทศนิยม
เพื่อให้มนุษย์โลกอ่านเข้าใจ
ในรูปฐาน 10 ก็จะได้เป็น
0.1000000000000000055511151231257827021181583404541015625
ทว่าคอมมันจะตัดให้เหลือแค่ 0.1 (คนจึงเห็นแค่นี้)
:
🤔 ซึ่งความเพื้ยนแบบนี้
แน่นอนทำให้เกิดบั๊กเวลาคำนวณตัวเลข
- ยิ่งงานต้องการคำตอบที่ละเอียดมาก เช่น งานธนาคาร ก็จะประสบปัญหา เป็นต้น
- หรือเวลานำไปใช้ในเงื่อนไขเปรียบเทียบพวก if, while ฯลฯ ก็อาจมีบั๊กเกิดขึ้นได้ เป็นต้น
😀 แต่ไม่ต้องห่วง ในหลายๆ ภาษาเขาจะมีวิธีแก้ปัญหานี้อยู่ครับ
ป้องกันการคำนวณตัวเลข ไม่ให้คลาดเคลื่อน เช่น
- ใน Java ก็จะมีคลาส BigDecimal เอาไว้บวกลบคูณหาร สำหรับเลขทศนิยมโดยเฉพาะ
- ใน Python ก็จะมีคลาสคล้ายๆ กัน เช่น Decimal
- ส่วนใน JavaScript อาจใช้ไลบรารี่ ซึ่งมีให้เลือกเยอะเช่น
https://github.com/MikeMcl/decimal.js/
https://github.com/MikeMcl/bignumber.js/
https://github.com/MikeMcl/big.js/
- ภาษาอื่นที่เหลือลองไปศึกษาเองดูนะครับ
เรื่องบวกลบคูณหาร เลขทศนิยม ถือเป็นเรื่องสำคัญที่ไม่ควรมองข้าม หากพลาดขึ้นมาแล้ว
สุดท้ายต้องมาไล่นั่งแก้โค้ดหลายบรรทัด
เสียเวลานั่งไล่ test ใหม่อีกรอบอีก
หมายเหตุเห็นคอมเมนต์สงสัยว่า
PHP กับ C # รอดชะตากรรมเดียวกันไหม ?
ก็บอกว่าไม่รอดครับ
// ลองดูตัวอย่างโค้ด C #
Console.WriteLine( ((0.1+0.2) == 0.3)); // False
Console.WriteLine( ((0.1+0.2) == 0.30000000000000004)); // True
// ลองดูตัวอย่างโค้ด PHP
echo number_format(0.1+0.2 , 17);
++++++
อ่าน IEEE-754 floating point ได้ที่
https://th.wikipedia.org/wiki/จำนวนจุดลอยตัว
ก่อนจากกันขอขายของก่อน
ถ้าสนใจหนังสือ จาวาสคริปต์ 700 กว่าหน้า เนื้อหาแน่น ขายดี ก็อุดหนุนได้ที่ MEB
👉 https://www.mebmarket.com/web/index.php?action=BookDetails&data=YToyOntzOjc6InVzZXJfaWQiO3M6NzoiMTcyNTQ4MyI7czo3OiJib29rX2lkIjtzOjY6IjE1Njg1NCI7fQ
เล่มนี้แบ่งเนื้อหาออกเป็น 2 ส่วน ได้แก่
✅ 1) ทบทวน JavaScript มาตรฐานเก่า
✅ 2) มาตรฐาน JavaScript สมัยใหม่ตั้งแต่ ES6 เป็นต้นไป
++++สารบัญ++++
📒 บทที่ 1 แนะนำจาวาสคริปต์
📒 บทที่ 2 ทบทวน HTML5 โดยย่อ (เพิ่มเข้ามาใหม่)
📒 บทที่ 3 รันจาวาสคริปต์อย่างง่าย (เพิ่มเข้ามาใหม่)
📒 บทที่ 4 ทบทวนมาตรฐานเก่า
📒 บทที่ 5 ทบทวนประโยคคำสั่งเบื้องต้น
📒 บทที่ 6 ทบทวนอ็อบเจ็กต์
📒 บทที่ 7 ทบทวน regex (เพิ่มเข้ามาใหม่)
📒 บทที่ 8 ฟีเจอร์ใหม่ของตัวเลข สตริง และ regex
📒 บทที่ 9 การประกาศตัวแปร และการกำหนดค่า
📒 บทที่ 10 ฟังก์ชั่น
📒 บทที่ 11 เทมเพลตสตริง
📒 บทที่ 12 ซิมโบล
📒 บทที่ 13 ฟีเจอร์ใหม่ของอ็อบเจ็กต์
📒 บทที่ 14 คลาส
📒 บทที่ 15 คอลเลคชั่น
📒 บทที่ 16 อิเทอเรเตอร์ และเจนเนอเรเตอร์
📒 บทที่ 17 เมต้าโปรแกรมมิ่ง
📒 บทที่ 18 พรอมิส
📒 บทที่ 19 การใช้งาน async กับ await (เพิ่มเข้ามาใหม่)
📒 บทที่ 20 มอดูล
✍ เขียนโดย โปรแกรมเมอร์ไทย thai Programmer Automation