คณิตศาสตร์เลขทศนิยมถูกหักหรือไม่


2983

พิจารณารหัสต่อไปนี้:

0.1 + 0.2 == 0.3  ->  false
0.1 + 0.2         ->  0.30000000000000004

ทำไมความไม่ถูกต้องเหล่านี้เกิดขึ้น


127
ตัวแปรจุดลอยตัวมักจะมีพฤติกรรมนี้ มันเกิดจากวิธีที่พวกเขาเก็บไว้ในฮาร์ดแวร์ สำหรับข้อมูลเพิ่มเติมตรวจสอบบทความวิกิพีเดียตัวเลขทศนิยม
Ben S

62
ถือว่า JavaScript ทศนิยมเป็นตัวเลขทศนิยมซึ่งหมายถึงการดำเนินการเช่นนอกจากนี้อาจมีการผิดพลาดในการปัดเศษ คุณอาจต้องการที่จะดูที่บทความนี้: อะไรทุกนักวิทยาศาสตร์คอมพิวเตอร์ควรรู้เกี่ยวกับจุดลอยเลขคณิต
ด้านข

4
สำหรับข้อมูลทุกประเภทตัวเลขใน javascript คือ IEEE-754 Doubles
Gary Willoughby

6
เนื่องจากจาวาสคริปต์ใช้มาตรฐาน IEEE 754 สำหรับคณิตศาสตร์มันใช้ตัวเลขลอยตัว64 บิต นี้ทำให้เกิดข้อผิดพลาดที่มีความแม่นยำเมื่อทำจุดลอยตัว (ทศนิยม) การคำนวณในระยะสั้นเนื่องจากคอมพิวเตอร์ทำงานในฐานที่ 2ในขณะที่เป็นทศนิยมฐาน 10
Pardeep Jain

คำตอบ:


2251

เลขทศนิยมเป็นเลขฐานสองแบบนี้ ในส่วนการเขียนโปรแกรมภาษามันอยู่บนพื้นฐานของมาตรฐาน IEEE 754 ปมของปัญหาคือตัวเลขที่แสดงในรูปแบบนี้เป็นจำนวนเต็มคูณด้วยกำลังสอง จำนวนตรรกยะ (เช่น0.1ซึ่งคือ1/10) ซึ่งตัวส่วนไม่ใช่พลังของสองไม่สามารถแสดงได้อย่างแน่นอน

สำหรับ0.1ในbinary64รูปแบบมาตรฐานการแทนค่าสามารถเขียนได้อย่างถูกต้องเหมือนกับ

  • 0.1000000000000000055511151231257827021181583404541015625 เป็นทศนิยมหรือ
  • 0x1.999999999999ap-4ในสัญกรณ์ hexfloat C99

ในทางตรงกันข้ามตัวเลขจำนวนตรรกยะ0.1ซึ่ง1/10สามารถเขียนได้อย่างแม่นยำว่า

  • 0.1 เป็นทศนิยมหรือ
  • 0x1.99999999999999...p-4ในอะนาล็อกของ C99 hexfloat สัญกรณ์ซึ่ง...หมายถึงลำดับที่ไม่สิ้นสุดของ 9

ค่าคงที่0.2และ0.3ในโปรแกรมของคุณจะประมาณค่าจริงของพวกเขา มันเกิดขึ้นที่ใกล้เคียงที่สุดdoubleที่จะ0.2มีขนาดใหญ่กว่าจำนวนที่มีเหตุผล0.2แต่ที่ใกล้เคียงที่สุดdoubleที่จะมีขนาดเล็กกว่าจำนวนที่มีเหตุผล0.3 0.3ผลรวมของ0.1และ0.2ลมขึ้นมีขนาดใหญ่กว่าจำนวนตรรกยะ0.3และไม่เห็นด้วยกับค่าคงที่ในรหัสของคุณ

การรักษาที่ครอบคลุมเป็นธรรมของปัญหาทางคณิตศาสตร์จุดลอยเป็นสิ่งที่ทุกนักวิทยาศาสตร์คอมพิวเตอร์ควรรู้เกี่ยวกับจุดลอยเลขคณิต สำหรับคำอธิบายที่ง่ายต่อการย่อยดูfloating-point-gui.de

หมายเหตุด้านข้าง: ระบบจำนวนตำแหน่งทั้งหมด (ฐาน - N) แบ่งปันปัญหานี้ได้อย่างแม่นยำ

ตัวเลขทศนิยมแบบธรรมดา (ฐาน 10) มีปัญหาเดียวกันซึ่งเป็นสาเหตุที่ตัวเลขเช่น 1/3 ท้ายเป็น 0.333333333 ...

คุณเพิ่งสะดุดกับตัวเลข (3/10) ที่ง่ายต่อการแสดงด้วยระบบทศนิยม แต่ไม่เหมาะกับระบบเลขฐานสอง มันไปทั้งสองทาง (ในระดับเล็ก ๆ ) เช่นกัน: 1/16 เป็นตัวเลขที่น่าเกลียดในหน่วยทศนิยม (0.0625) แต่ในรูปแบบไบนารีมันดูเรียบร้อยเท่าที่ 10,000 ในรูปทศนิยม (0.0001) ** - ถ้าเราอยู่ใน นิสัยของการใช้ระบบเลขฐาน 2 ในชีวิตประจำวันของเราคุณอาจลองดูตัวเลขนั้นและเข้าใจโดยสัญชาตญาณว่าคุณจะมาถึงที่นั่นด้วยการลดบางสิ่งลงครึ่งหนึ่งอีกครั้งและอีกครั้ง

** แน่นอนว่านั่นไม่ใช่วิธีการจัดเก็บตัวเลขทศนิยมในหน่วยความจำ (พวกเขาใช้รูปแบบของสัญลักษณ์ทางวิทยาศาสตร์) อย่างไรก็ตามมันแสดงให้เห็นถึงจุดที่ข้อผิดพลาดความแม่นยำจุดลอยตัวไบนารีมีแนวโน้มที่จะครอบตัดขึ้นเพราะตัวเลข "โลกแห่งความจริง" ที่เรามักจะสนใจในการทำงานมักจะมีพลังถึงสิบ - แต่เพราะเราใช้ระบบเลขทศนิยมวัน - ต่อวัน นี่คือเหตุผลที่เราจะพูดถึงสิ่งต่าง ๆ เช่น 71% แทนที่จะเป็น "5 จากทุก ๆ 7" (71% เป็นค่าประมาณเนื่องจาก 5/7 ไม่สามารถแสดงด้วยเลขทศนิยมได้อย่างแม่นยำ)

ไม่เลย: เลขทศนิยมเลขฐานสองไม่แตกพวกมันเพิ่งจะไม่สมบูรณ์เหมือนระบบเลขฐาน N อื่น ๆ :)

หมายเหตุด้านข้าง: การทำงานกับโฟลทในการเขียนโปรแกรม

ในทางปฏิบัติปัญหาความแม่นยำนี้หมายความว่าคุณจำเป็นต้องใช้ฟังก์ชันการปัดเศษเพื่อปัดเศษตัวเลขทศนิยมของคุณออกเป็นทศนิยมหลายตำแหน่งที่คุณสนใจก่อนที่จะแสดง

คุณต้องแทนที่การทดสอบความเท่าเทียมกันด้วยการเปรียบเทียบที่อนุญาตให้มีจำนวนความอดทนซึ่งหมายความว่า:

ไม่ได้ทำif (x == y) { ... }

if (abs(x - y) < myToleranceValue) { ... }แทนที่จะทำ

โดยที่absเป็นค่าสัมบูรณ์ myToleranceValueจำเป็นต้องได้รับการคัดเลือกสำหรับแอปพลิเคชันเฉพาะของคุณ - และจะมีหลายอย่างที่เกี่ยวข้องกับ "ห้องเลื้อย" ที่คุณเตรียมไว้เพื่อให้อนุญาต ) ระวังค่าคงสไตล์ "epsilon" ในภาษาที่คุณเลือก สิ่งเหล่านี้ไม่ควรใช้เป็นค่าความอดทน


181
ฉันคิดว่า "ค่าความผิดพลาดคงที่" นั้นถูกต้องมากกว่า "The Epsilon" เพราะไม่มี "The Epsilon" ซึ่งสามารถใช้ได้ในทุกกรณี epsilons ที่แตกต่างกันจะต้องใช้ในสถานการณ์ที่แตกต่างกัน และเครื่อง epsilon แทบจะไม่เคยมีค่าคงที่ที่จะใช้
Rotsor

34
มันไม่เป็นความจริงเลยที่คณิตศาสตร์เลขทศนิยมทั้งหมดนั้นยึดตามมาตรฐาน IEEE [754] ยังมีบางระบบที่ใช้งานซึ่งมี IBM hexadecimal FP เก่าเช่นและยังมีการ์ดกราฟิกที่ไม่รองรับเลขคณิต IEEE-754 อย่างไรก็ตามมันเป็นความจริงสำหรับการประมาณที่สมเหตุสมผล
สตีเฟ่นแคนนอน

19
Cray ได้ทำตามมาตรฐาน IEEE-754 Java คลายการยึดมั่นในการเพิ่มประสิทธิภาพเช่นกัน
เทย์เลอร์

28
ฉันคิดว่าคุณควรเพิ่มบางอย่างในคำตอบนี้เกี่ยวกับวิธีการคำนวณเงินควรทำด้วยเลขคณิตคงที่ของจำนวนเต็มเสมอเพราะเงินนั้นถูกคำนวณ (มันอาจสมเหตุสมผลที่จะทำการคำนวณทางบัญชีภายในด้วยเศษเสี้ยวเล็ก ๆ ของหน่วยเปอร์เซ็นต์หรือหน่วยสกุลเงินที่เล็กที่สุดของคุณ - สิ่งนี้มักจะช่วยเช่นลดข้อผิดพลาดปัดเศษเมื่อแปลง "$ 29.99 ต่อเดือน" เป็นอัตรารายวัน - แต่ควร ยังคงเป็นเลขคณิตคงที่).
zwol

18
ข้อเท็จจริงที่น่าสนใจ: สิ่งนี้ไม่ได้แสดงถึงจุดลอยตัวแบบไบนารีเพียงอย่างเดียวทำให้เกิดข้อผิดพลาดของซอฟต์แวร์ต่อต้านขีปนาวุธรักชาติซึ่งส่งผลให้มีผู้เสียชีวิต 28 รายในช่วงสงครามอิรักครั้งแรก
hdl

602

มุมมองของนักออกแบบฮาร์ดแวร์

ฉันเชื่อว่าฉันควรเพิ่มมุมมองของนักออกแบบฮาร์ดแวร์เนื่องจากฉันออกแบบและสร้างฮาร์ดแวร์จุดลอยตัว การทราบที่มาของข้อผิดพลาดอาจช่วยในการทำความเข้าใจสิ่งที่เกิดขึ้นในซอฟต์แวร์และท้ายที่สุดฉันหวังว่าสิ่งนี้จะช่วยอธิบายสาเหตุที่ทำให้เกิดข้อผิดพลาดจุดลอยตัวและดูเหมือนจะสะสมอยู่ตลอดเวลา

1. ภาพรวม

จากมุมมองทางวิศวกรรมการดำเนินการจุดลอยตัวส่วนใหญ่จะมีองค์ประกอบของข้อผิดพลาดเนื่องจากฮาร์ดแวร์ที่ใช้การคำนวณจุดลอยตัวจำเป็นต้องมีข้อผิดพลาดน้อยกว่าครึ่งหนึ่งของหนึ่งหน่วยในสถานที่สุดท้าย ดังนั้นฮาร์ดแวร์จำนวนมากจะหยุดที่ความแม่นยำที่จำเป็นเท่านั้นที่จะให้ข้อผิดพลาดน้อยกว่าครึ่งหนึ่งของหน่วยในสถานที่สุดท้ายสำหรับการดำเนินการเดียวซึ่งเป็นปัญหาโดยเฉพาะอย่างยิ่งในการแบ่งจุดลอยตัว สิ่งที่ถือว่าเป็นการดำเนินการครั้งเดียวขึ้นอยู่กับจำนวนตัวถูกดำเนินการหน่วยที่ใช้ ส่วนใหญ่เป็นสอง แต่บางหน่วยใช้ตัวถูกดำเนินการ 3 ตัวขึ้นไป ด้วยเหตุนี้จึงไม่มีการรับประกันว่าการทำงานซ้ำ ๆ จะส่งผลให้เกิดข้อผิดพลาดที่พึงประสงค์เนื่องจากข้อผิดพลาดจะเพิ่มขึ้นเมื่อเวลาผ่านไป

2. มาตรฐาน

โปรเซสเซอร์ส่วนใหญ่ตามมาตรฐาน IEEE-754แต่บางตัวใช้ denormalized หรือมาตรฐานที่แตกต่างกัน ตัวอย่างเช่นมีโหมด denormalized ใน IEEE-754 ซึ่งอนุญาตให้แสดงหมายเลขจุดลอยตัวที่น้อยมากโดยใช้ความแม่นยำ อย่างไรก็ตามต่อไปนี้จะครอบคลุมโหมดการทำให้เป็นมาตรฐานของ IEEE-754 ซึ่งเป็นโหมดการทำงานทั่วไป

ในมาตรฐาน IEEE-754 นักออกแบบฮาร์ดแวร์ได้รับอนุญาตให้มีข้อผิดพลาด / epsilon ใด ๆ ตราบใดที่มันน้อยกว่าครึ่งหนึ่งของหนึ่งหน่วยในสถานที่สุดท้ายและผลลัพธ์จะต้องน้อยกว่าครึ่งหนึ่งของหนึ่งหน่วยในที่สุด สถานที่สำหรับการดำเนินการอย่างใดอย่างหนึ่ง สิ่งนี้อธิบายว่าทำไมเมื่อมีการดำเนินการซ้ำหลายครั้งข้อผิดพลาดจะเพิ่มขึ้น สำหรับความแม่นยำสองเท่าของ IEEE-754 นี่คือบิตที่ 54 เนื่องจาก 53 บิตถูกใช้เพื่อแทนส่วนที่เป็นตัวเลข (ปกติ) หรือที่เรียกว่า mantissa ของจำนวนจุดลอยตัว (เช่น 5.3 ใน 5.3e5) ส่วนต่อไปจะกล่าวถึงรายละเอียดเพิ่มเติมเกี่ยวกับสาเหตุของข้อผิดพลาดของฮาร์ดแวร์ในการดำเนินการจุดลอยตัวต่างๆ

3. สาเหตุของข้อผิดพลาดในการปัดเศษในแผนก

สาเหตุหลักของข้อผิดพลาดในการหารทศนิยมเป็นอัลกอริทึมการหารที่ใช้ในการคำนวณความฉลาด ระบบคอมพิวเตอร์ส่วนใหญ่คำนวณการหารด้วยการคูณด้วยอินเวอร์Z=X/YZ = X * (1/Y)ส การหารถูกคำนวณซ้ำ ๆ เช่นแต่ละรอบจะคำนวณบางส่วนของความฉลาดจนกว่าจะถึงความแม่นยำที่ต้องการซึ่งสำหรับ IEEE-754 นั้นเป็นทุกอย่างที่มีข้อผิดพลาดน้อยกว่าหนึ่งหน่วยในสถานที่สุดท้าย ตารางของส่วนกลับของ Y (1 / Y) เป็นที่รู้จักกันในชื่อ Quote Selection Table (QST) ในส่วนที่ช้าและขนาดในหน่วยบิตของตารางการเลือกความฉลาดมักจะเป็นความกว้างของฐานหรือจำนวนบิตของ ความฉลาดทางที่คำนวณได้ในการคำนวณซ้ำแต่ละครั้งบวกบิตยามสองสามตัว สำหรับมาตรฐาน IEEE-754 ความแม่นยำสองเท่า (64- บิต) มันจะเป็นขนาดของ radix ของตัวแบ่งบวกสองบิตตัวป้องกัน k ที่k>=2. ตัวอย่างเช่นตารางการเลือกความฉลาดทางทั่วไปสำหรับตัวหารที่คำนวณ 2 บิตของความฉลาดในแต่ละครั้ง (radix 4) จะเป็น2+2= 4บิต (บวกบิตเสริมบางอย่าง)

3.1 Division Rounding Error: การประมาณค่ากลับไปกลับมา

สิ่งที่ตอบกลับอยู่ในตารางการเลือกความฉลาดทางขึ้นอยู่กับวิธีการหาร : การหารช้าเช่นการแบ่ง SRT หรือการแบ่งอย่างรวดเร็วเช่นการแบ่ง Goldschmidt; แต่ละรายการมีการแก้ไขตามอัลกอริทึมการหารในความพยายามที่จะทำให้เกิดข้อผิดพลาดต่ำที่สุด อย่างไรก็ตามในกรณีใด ๆ การแลกเปลี่ยนทั้งหมดเป็นการประมาณของการแลกเปลี่ยนซึ่งกันและกันและแนะนำองค์ประกอบของข้อผิดพลาด ทั้งการหารช้าและวิธีการหารอย่างรวดเร็วคำนวณความฉลาดทางซ้ำนั่นคือจำนวนบิตของความฉลาดทางคำนวณในแต่ละขั้นตอนจากนั้นผลลัพธ์จะถูกลบออกจากเงินปันผลและตัวหารจะทำซ้ำขั้นตอนจนกว่าข้อผิดพลาดจะน้อยกว่าครึ่งหนึ่ง หน่วยในสถานที่สุดท้าย วิธีการหารแบบช้าจะคำนวณจำนวนหลักของความฉลาดทางในแต่ละขั้นตอนและมักจะไม่แพงในการสร้างและวิธีการหารอย่างรวดเร็วจะคำนวณจำนวนตัวเลขของตัวเลขต่อขั้นและมักจะมีราคาแพงกว่าในการสร้าง ส่วนที่สำคัญที่สุดของวิธีการแบ่งคือส่วนใหญ่ของพวกเขาพึ่งพาการคูณซ้ำโดยการประมาณซึ่งกันและกันดังนั้นพวกเขาจึงมีแนวโน้มที่จะเกิดข้อผิดพลาด

4. การปัดเศษข้อผิดพลาดในการดำเนินการอื่น: การตัดปลาย

สาเหตุของข้อผิดพลาดในการปัดเศษในการดำเนินการทั้งหมดคือโหมดที่แตกต่างกันของการตัดปลายของคำตอบสุดท้ายที่ IEEE-754 อนุญาต มีการตัดปลายปัดเศษเป็นศูนย์ปัดเศษใกล้สุด (ค่าเริ่มต้น)ปัดเศษขึ้นและปัดเศษขึ้น วิธีการทั้งหมดแนะนำองค์ประกอบของข้อผิดพลาดน้อยกว่าหนึ่งหน่วยในสถานที่สุดท้ายสำหรับการดำเนินการเดียว เมื่อเวลาผ่านไปและการดำเนินการซ้ำแล้วซ้ำอีกการตัดปลายยังเพิ่มข้อผิดพลาดผลลัพธ์ การตัดทอนข้อผิดพลาดนี้เป็นปัญหาอย่างยิ่งในการยกกำลังซึ่งเกี่ยวข้องกับการคูณซ้ำหลายรูปแบบ

5. การดำเนินการซ้ำแล้วซ้ำอีก

เนื่องจากฮาร์ดแวร์ที่ใช้การคำนวณจุดลอยตัวจำเป็นต้องให้ผลลัพธ์ที่มีข้อผิดพลาดน้อยกว่าครึ่งหนึ่งของหนึ่งหน่วยในตำแหน่งสุดท้ายสำหรับการดำเนินการครั้งเดียวข้อผิดพลาดจะเพิ่มขึ้นจากการดำเนินการซ้ำ ๆ นี่คือเหตุผลที่ในการคำนวณที่ต้องการข้อผิดพลาดขอบเขตนักคณิตศาสตร์ใช้วิธีการเช่นการใช้เลขคู่ที่ใกล้เคียงที่สุดในสถานที่สุดท้ายของ IEEE-754 เพราะเมื่อเวลาผ่านไปข้อผิดพลาดมีแนวโน้มที่จะยกเลิกกัน ออกและช่วงเวลาทางคณิตศาสตร์รวมกับรูปแบบของการปัดเศษ IEEE 754เพื่อทำนายข้อผิดพลาดในการปัดเศษและแก้ไขให้ถูกต้อง เนื่องจากข้อผิดพลาดสัมพัทธ์ต่ำเมื่อเทียบกับโหมดการปัดเศษอื่น ๆ การปัดเศษเป็นเลขคู่ที่ใกล้ที่สุด (ในตำแหน่งสุดท้าย) คือโหมดการปัดเศษเริ่มต้นของ IEEE-754

โปรดทราบว่าโหมดการปัดเศษเริ่มต้นซึ่งเป็นเลขคู่ที่ใกล้ที่สุดถึงจุดสุดท้ายรับประกันความผิดพลาดน้อยกว่าครึ่งหนึ่งของหนึ่งหน่วยในตำแหน่งสุดท้ายสำหรับการดำเนินการครั้งเดียว การใช้การปัดเศษปัดเศษขึ้นและปัดเศษตามลำพังอาจส่งผลให้เกิดข้อผิดพลาดที่มากกว่าครึ่งหนึ่งของหนึ่งหน่วยในสถานที่สุดท้าย แต่น้อยกว่าหนึ่งหน่วยในสถานที่สุดท้ายดังนั้นจึงไม่แนะนำให้ใช้โหมดเหล่านี้ ใช้ในการคำนวณช่วงเวลา

6. สรุป

ในระยะสั้นเหตุผลพื้นฐานสำหรับข้อผิดพลาดในการดำเนินการจุดลอยตัวคือการรวมกันของการตัดทอนในฮาร์ดแวร์และการตัดทอนของซึ่งกันและกันในกรณีของการแบ่ง เนื่องจากมาตรฐาน IEEE-754 ต้องการเพียงข้อผิดพลาดน้อยกว่าครึ่งหนึ่งของหน่วยในสถานที่สุดท้ายสำหรับการดำเนินการเดียวข้อผิดพลาดจุดลอยตัวมากกว่าการดำเนินการซ้ำจะเพิ่มขึ้นเว้นแต่จะได้รับการแก้ไข


8
(3) ผิด ข้อผิดพลาดในการปัดเศษในส่วนไม่น้อยกว่าหนึ่งหน่วยในสถานที่สุดท้าย แต่อย่างน้อยครึ่งหน่วยในสถานที่สุดท้าย
gnasher729

6
@ gnasher729 จับได้ดี การดำเนินการขั้นพื้นฐานส่วนใหญ่มีข้อผิดพลาด en น้อยกว่า 1/2 ของหนึ่งหน่วยในสถานที่สุดท้ายโดยใช้โหมดการปัดเศษ IEEE ที่เป็นค่าเริ่มต้น แก้ไขคำอธิบายและระบุว่าข้อผิดพลาดอาจมากกว่า 1/2 ของหนึ่ง ulp แต่น้อยกว่า 1 ulp หากผู้ใช้แทนที่โหมดการปัดเศษเริ่มต้น (นี่เป็นจริงโดยเฉพาะในระบบฝังตัว)
KernelPanik

39
(1) หมายเลขจุดลอยตัวไม่มีข้อผิดพลาด ค่าจุดลอยตัวทุกค่าเป็นสิ่งที่เป็นจริง การดำเนินการจุดลอยตัวส่วนใหญ่ (แต่ไม่ใช่ทั้งหมด) ให้ผลลัพธ์ที่ไม่แน่นอน ตัวอย่างเช่นไม่มีค่าทศนิยมแบบไบนารีที่เท่ากับ 1.0 / 10.0 การดำเนินการบางอย่าง (เช่น 1.0 + 1.0) จะให้ผลลัพธ์ที่แน่นอนในทางกลับกัน
โซโลมอนช้า

19
"สาเหตุหลักของข้อผิดพลาดในการหารทศนิยมเป็นอัลกอริทึมการหารที่ใช้ในการคำนวณความฉลาด" เป็นสิ่งที่ทำให้เข้าใจผิดมากที่จะพูด สำหรับส่วนที่สอดคล้องตามมาตรฐาน IEEE-754 สาเหตุของข้อผิดพลาดเพียงอย่างเดียวในการหารจุดลอยตัวคือการไม่สามารถแสดงผลลัพธ์ได้อย่างแม่นยำในรูปแบบผลลัพธ์ คำนวณผลลัพธ์เดียวกันโดยไม่คำนึงถึงอัลกอริทึมที่ใช้
สตีเฟ่นแคนนอน

6
@Matt ขออภัยในความล่าช้า มันเป็นเพราะปัญหาทรัพยากร / เวลาและการแลกเปลี่ยน มีวิธีที่จะทำการแบ่งแบบยาว / การแบ่งแบบ 'ปกติ' มากขึ้นซึ่งเรียกว่าการแบ่งส่วนแบบ SRT ด้วยเลขฐานสอง อย่างไรก็ตามสิ่งนี้จะเลื่อนและลบตัวหารออกจากการจ่ายเงินปันผลซ้ำ ๆ และใช้รอบสัญญาณนาฬิกาหลายรอบเนื่องจากมันจะคำนวณความฉลาดทางหนึ่งบิตต่อรอบนาฬิกา เราใช้ตารางส่วนกลับเพื่อให้เราสามารถคำนวณบิตของเชาวน์ต่อรอบและทำการแลกเปลี่ยนประสิทธิภาพ / ความเร็วอย่างมีประสิทธิภาพ
KernelPanik

462

เมื่อคุณแปลง. 1 หรือ 1/10 เป็นฐาน 2 (เลขฐานสอง) คุณจะได้รูปแบบการทำซ้ำหลังจุดทศนิยมเช่นเดียวกับการพยายามแทนค่า 1/3 ในฐาน 10 ค่าไม่ถูกต้องและคุณไม่สามารถทำได้ คณิตศาสตร์ที่แน่นอนกับมันโดยใช้วิธีการจุดลอยปกติ


133
คำตอบที่ดีและสั้น การทำซ้ำรูปแบบลักษณะเหมือน 0,00011001100110011001100110011001100110011001100110011 ...
คอนสแตนติ Chernov

4
สิ่งนี้ไม่ได้อธิบายว่าทำไมไม่ใช้อัลกอริทึมที่ดีกว่าซึ่งไม่ได้แปลงเป็นไบนารีตั้งแต่แรก
Dmitri Zaitsev

12
เพราะประสิทธิภาพ การใช้ไบนารี่นั้นเร็วกว่าสองสามพันเท่าเพราะมันใช้กับเครื่อง
Joel Coehoorn

7
มีวิธีการที่ให้ค่าทศนิยมที่แน่นอน BCD (ทศนิยมแบบไบนารีไบนารี่) หรือรูปแบบอื่น ๆ ของตัวเลขทศนิยม อย่างไรก็ตามสิ่งเหล่านี้จะช้ากว่า (ช้ากว่ามาก) และใช้พื้นที่เก็บข้อมูลมากกว่าการใช้เลขทศนิยมแบบไบนารี (เป็นตัวอย่าง BCD ที่บรรจุจะจัดเก็บตัวเลขทศนิยม 2 หลักในไบต์นั่นคือ 100 ค่าที่เป็นไปได้ในไบต์ที่จริงสามารถเก็บค่าที่เป็นไปได้ 256 ค่าหรือ 100/256 ซึ่งเปลืองประมาณ 60% ของค่าที่เป็นไปได้ของไบต์)
Duncan C

16
@ Jacksonkr คุณยังคิดอยู่ในฐาน -10 คอมพิวเตอร์เป็นฐาน 2
Joel Coehoorn

306

คำตอบส่วนใหญ่ที่นี่ตอบคำถามนี้ในเงื่อนไขทางเทคนิคที่แห้งมาก ฉันต้องการพูดถึงเรื่องนี้ในแง่ที่ว่ามนุษย์ทั่วไปสามารถเข้าใจได้

ลองนึกภาพว่าคุณกำลังพยายามหั่นพิซซ่า คุณมีเครื่องตัดพิซซ่าหุ่นยนต์ที่สามารถตัดพิซซ่าได้ครึ่งส่วน มันอาจลดลงครึ่งหนึ่งของพิซซ่าทั้งหมดหรือลดลงครึ่งหนึ่งของชิ้นที่มีอยู่ แต่ไม่ว่าในกรณีใดการแบ่งครึ่งจะแน่นอนเสมอ

ที่ตัดพิซซ่ามีการเคลื่อนไหวที่ดีมากและหากคุณเริ่มต้นด้วยพิซซ่าทั้งหมดแล้วลดลงครึ่งหนึ่งนั้นและยังคงลดลงครึ่งหนึ่งชิ้นที่เล็กที่สุดในแต่ละครั้งที่คุณสามารถทำได้ลดลงครึ่งหนึ่ง53 ครั้งก่อนที่จะชิ้นมีขนาดเล็กเกินไปสำหรับแม้แต่ความสามารถที่มีความแม่นยำสูงของ . ณ จุดนี้คุณจะไม่สามารถแบ่งส่วนที่บางมากออกไปได้อีกต่อไป แต่จะต้องรวมหรือแยกออกตามที่เป็นอยู่

ทีนี้คุณจะทำชิ้นส่วนทั้งหมดด้วยวิธีที่จะเพิ่มพิซซ่าหนึ่งในสิบ (0.1) หรือหนึ่งในห้า (0.2) ของพิซซ่าได้อย่างไร ลองคิดดูสิและลองทำดู คุณสามารถลองใช้พิซซ่าจริงได้หากคุณมีเครื่องตัดพิซซ่าที่มีความแม่นยำสูงในตำนาน :-)


โปรแกรมเมอร์ที่มีประสบการณ์มากที่สุดของหลักสูตรรู้คำตอบที่แท้จริงซึ่งก็คือว่ามีวิธีที่จะชิ้นด้วยกันแน่นอนที่สิบหรือห้าของพิซซ่าโดยใช้ชิ้นเหล่านั้นไม่ว่าประณีตคุณ slice พวกเขา คุณสามารถทำการประมาณที่ดีทีเดียวและถ้าคุณบวกการประมาณ 0.1 เข้ากับการประมาณ 0.2 คุณจะได้การประมาณที่ดีที่ 0.3 แต่มันก็แค่การประมาณ

สำหรับตัวเลขที่มีความแม่นยำสองเท่า (ซึ่งเป็นความแม่นยำที่อนุญาตให้คุณลดจำนวนพิซซ่าลง 53 เท่า) จำนวนที่น้อยกว่าและมากกว่า 0.1 คือ อันหลังนั้นใกล้กว่า 0.1 เล็กน้อยก่อนดังนั้นตัวแยกวิเคราะห์ตัวเลขจะให้อินพุต 0.1 ให้การสนับสนุนหลัง

(ความแตกต่างระหว่างตัวเลขทั้งสองนี้คือ "ชิ้นเล็กที่สุด" ที่เราต้องตัดสินใจว่าจะรวมไว้ซึ่งจะทำให้เกิดอคติขึ้นหรือแยกออกซึ่งทำให้เกิดอคติลงคำศัพท์ทางเทคนิคสำหรับชิ้นเล็กที่สุดนั้นคือulp )

ในกรณีของ 0.2 ตัวเลขนั้นเหมือนกันทั้งหมดเพียงแค่เพิ่มขนาดเป็น 2 อีกครั้งเราชอบค่าที่สูงกว่า 0.2 เล็กน้อย

โปรดสังเกตว่าในทั้งสองกรณีการประมาณค่าสำหรับ 0.1 และ 0.2 มีอคติสูงขึ้นเล็กน้อย หากเราเพิ่มความเอนเอียงเหล่านี้มากพอพวกเขาจะผลักดันตัวเลขให้ห่างออกไปและไกลออกไปจากสิ่งที่เราต้องการและอันที่จริงแล้วในกรณีของ 0.1 + 0.2 อคตินั้นสูงพอที่จำนวนผลลัพธ์จะไม่ใกล้เคียงที่สุด ถึง 0.3

โดยเฉพาะอย่างยิ่ง 0.1 + 0.2 จริง ๆ แล้วคือ 0.10000000000000000000055511151231257827021181583404541015625 + 0.200000099999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999888 โดยเฉพาะอย่างยิ่ง 0.1 + 0.2 จริง ๆ แล้วคือ 0.10000000000000000000055511151231257827021181583404541015625 + 0.20000009999999999899999999999999999999999999999999999999999999999999999999999999999999999999999999888 โดยเฉพาะอย่างยิ่ง 0.1 + 0.2 จริง ๆ คือ 0.10000000000000000000055511151231257827021181583404541015625


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

(โพสต์ครั้งแรกบน Quora)


3
โปรดทราบว่ามีบางภาษาซึ่งรวมถึงคณิตศาสตร์ที่แน่นอน ตัวอย่างหนึ่งคือ Scheme ตัวอย่างเช่นผ่าน GNU Guile ดูdraketo.de/english/exact-math-to-the-rescue - สิ่งเหล่านี้ทำให้คณิตศาสตร์เป็นเศษส่วนและฝืดขึ้นมาในที่สุดเท่านั้น
Arne Babenhauserheide

5
@FloatingRock ที่จริงแล้วภาษาการเขียนโปรแกรมหลักน้อยมากมีตัวเลขที่มีเหตุผลอยู่ภายใน Arne เป็น Schemer อย่างที่ฉันเป็นดังนั้นสิ่งเหล่านี้จึงทำให้เราเสีย
Chris Jester-Young

5
@ ArneBabenhauserheide ฉันคิดว่ามันคุ้มค่าที่จะใช้กับตัวเลขที่มีเหตุผลเท่านั้น ดังนั้นถ้าคุณกำลังทำคณิตศาสตร์ด้วยตัวเลขที่ไม่ลงตัวเช่น pi คุณต้องเก็บมันไว้เป็นไพหลาย ๆ แน่นอนการคำนวณใด ๆ ที่เกี่ยวข้องกับ pi ไม่สามารถแสดงเป็นจำนวนทศนิยมที่แน่นอน
Aidiakapi

13
@connexo โอเค คุณจะตั้งโปรแกรม rotator พิซซ่าอย่างไรให้ได้ 36 องศา? 36 องศาคืออะไร (คำแนะนำ: หากคุณสามารถกำหนดสิ่งนี้ในแบบที่แน่นอนคุณยังมีเครื่องตัดพิซซ่าชิ้นหนึ่งและที่แน่นอนสิบ) ในคำอื่น ๆ คุณไม่สามารถมี 1/360 (องศา) หรือ 1 / 10 (36 องศา) ที่มีเฉพาะจุดลอยตัวไบนารี
Chris Jester-Young

12
@connexo "คนบ้าทุกคน" ไม่สามารถหมุนพิซซ่าได้ 36 องศา มนุษย์ผิดพลาดง่ายเกินไปที่จะทำอะไรที่แม่นยำมาก
Chris Jester-Young

212

ข้อผิดพลาดในการปัดเศษทศนิยม 0.1 ไม่สามารถแสดงได้อย่างถูกต้องในฐาน -2 ในขณะที่ฐาน 10 เนื่องจากปัจจัยหลักที่ขาดหายไปของ 5 เช่นเดียวกับ 1/3 ใช้จำนวนอนันต์ของตัวเลขเพื่อแสดงในทศนิยม แต่เป็น "0.1" ในฐาน -3 0.1 รับจำนวนอนันต์ของตัวเลขในฐาน 2 โดยที่ไม่ได้อยู่ในฐาน -10 และคอมพิวเตอร์ไม่มีหน่วยความจำไม่ จำกัด


133
คอมพิวเตอร์ไม่ต้องการหน่วยความจำไม่ จำกัด เพื่อรับ 0.1 + 0.2 = 0.3 ถูกต้อง
Pacerier

23
@Pierier แน่นอนว่าพวกเขาสามารถใช้จำนวนเต็มสองจำนวนที่มีความแม่นยำไม่ จำกัด เพื่อแสดงถึงเศษส่วนหรือพวกเขาอาจใช้เครื่องหมายคำพูด มันเป็นความคิดที่เฉพาะเจาะจงของ "เลขฐานสอง" หรือ "ทศนิยม" ที่ทำให้เป็นไปไม่ได้ - ความคิดที่ว่าคุณมีลำดับของเลขฐานสอง / เลขฐานสิบและจุดใดจุดหนึ่งในนั้น เพื่อให้ได้ผลลัพธ์ที่มีเหตุผลอย่างแม่นยำเราต้องการรูปแบบที่ดีกว่า
Devin Jeanpierre

15
@Pacerier: เลขฐานสองหรือทศนิยมไม่สามารถเก็บได้อย่างแม่นยำ 1/3 หรือ 1/13 ทศนิยมชนิดทศนิยมสามารถแทนค่าของรูปแบบ M / 10 ^ E ได้อย่างแม่นยำ แต่มีความแม่นยำน้อยกว่าตัวเลขทศนิยมเลขฐานสองขนาดใกล้เคียงกันเมื่อมันแสดงถึงเศษส่วนอื่น ๆ ส่วนใหญ่ ในหลาย ๆ แอปพลิเคชั่นมันมีประโยชน์มากกว่าที่จะมีความแม่นยำสูงกว่าด้วยเศษส่วนโดยพลการ
supercat

13
@Pierier พวกเขาทำถ้าพวกเขาเก็บตัวเลขเป็นเลขฐานสองลอยซึ่งเป็นจุดของคำตอบ
Mark Amery

3
@ chux: ความแตกต่างของความแม่นยำระหว่างประเภทไบนารีและทศนิยมนั้นไม่มากนัก แต่ความแตกต่างระหว่าง 10: 1 ในกรณีที่ดีที่สุดเทียบกับความแม่นยำของตัวพิมพ์เล็กที่สุดสำหรับประเภททศนิยมนั้นมากกว่าความแตกต่าง 2: 1 กับประเภทไบนารี ฉันอยากรู้ว่าใครก็ตามที่สร้างฮาร์ดแวร์หรือซอฟต์แวร์ที่เขียนขึ้นเพื่อให้ทำงานได้อย่างมีประสิทธิภาพกับทศนิยมชนิดใดประเภทหนึ่งเนื่องจากดูเหมือนว่าทั้งสองจะไม่ตอบสนองต่อการใช้งานที่มีประสิทธิภาพในฮาร์ดแวร์หรือซอฟต์แวร์
supercat

121

นอกเหนือจากคำตอบที่ถูกต้องอื่น ๆ คุณอาจต้องการพิจารณาปรับขนาดค่าของคุณเพื่อหลีกเลี่ยงปัญหาเกี่ยวกับการคำนวณเลขทศนิยม

ตัวอย่างเช่น:

var result = 1.0 + 2.0;     // result === 3.0 returns true

... แทน:

var result = 0.1 + 0.2;     // result === 0.3 returns false

การแสดงออก0.1 + 0.2 === 0.3กลับมาfalseใน JavaScript แต่โชคดีที่เลขจำนวนเต็มในทศนิยมเป็นที่แน่นอนดังนั้นข้อผิดพลาดในการแทนทศนิยมสามารถหลีกเลี่ยงได้โดยการปรับขนาด

เพื่อเป็นตัวอย่างในทางปฏิบัติเพื่อหลีกเลี่ยงปัญหาจุดลอยตัวที่ความแม่นยำเป็นสิ่งสำคัญที่สุดขอแนะนำ1ให้จัดการเงินเป็นจำนวนเต็มแทนจำนวนเซ็นต์: 2550เซ็นต์แทนที่จะเป็น25.50ดอลลาร์


1ดักลาส Crockford: JavaScript: ดีอะไหล่ : ภาคผนวก A - อะไหล่ที่น่ากลัว (หน้า 105)


3
ปัญหาคือการแปลงตัวเองไม่ถูกต้อง 16.08 * 100 = 1607.9999999999998 เราต้องแยกจำนวนและแปลงแยกต่างหาก (เช่นใน 16 * 100 + 08 = 1608) หรือไม่?
Jason

38
วิธีแก้ปัญหาที่นี่คือทำการคำนวณทั้งหมดของคุณเป็นจำนวนเต็มแล้วหารด้วยสัดส่วนของคุณ (100 ในกรณีนี้) และปัดเศษเฉพาะเมื่อแสดงข้อมูล เพื่อให้แน่ใจว่าการคำนวณของคุณจะแม่นยำเสมอ
David Granado

15
เพียงเพื่อ nitpick เล็กน้อย: เลขคณิตเลขจำนวนเต็มเป็นเพียงจุดลอยตัวขึ้นไปถึงจุด (ปุนตั้งใจ) หากตัวเลขมีขนาดใหญ่กว่า 0x1p53 (เพื่อใช้เครื่องหมายทศนิยมเลขฐานสิบหกของ Java 7, = 9007199254740992) แสดงว่า ulp คือ 2 ที่จุดนั้นดังนั้น 0x1p53 + 1 จะถูกปัดเศษเป็น 0x1p53 + และ 0x1p53 + 3 จะถูกปัดเศษเป็น 0x1p53 + 3 4 เนื่องจากรอบต่อเนื่อง) :-D แต่แน่นอนถ้าหมายเลขของคุณน้อยกว่า 9 quadrillion คุณควรจะดี :-P
Chris Jester-Young

2
เจสันคุณควรปัดเศษผลลัพธ์ (int) (16.08 * 100 + 0.5)
Mikhail Semenov

@CodyBugstein " ดังนั้นคุณจะได้รับ. 1 + .2 เพื่อแสดง. 3 ได้อย่างไร " เขียนฟังก์ชั่นการพิมพ์แบบกำหนดเองเพื่อวางทศนิยมตามที่คุณต้องการ
RonJohn

113

คำตอบของฉันค่อนข้างยาวดังนั้นฉันจึงแบ่งออกเป็นสามส่วน เนื่องจากคำถามเกี่ยวกับคณิตศาสตร์จุดลอยตัวฉันได้ให้ความสำคัญกับสิ่งที่เครื่องทำจริง ฉันได้ทำให้มันมีความแม่นยำเป็นสองเท่า (64 บิต) แต่ข้อโต้แย้งนี้ใช้กับเลขคณิตจุดลอยตัวอย่างเท่าเทียมกัน

คำนำ

IEEE 754 แม่นยำสองรูปแบบไบนารีจุดลอยตัว (binary64)จำนวนหมายถึงจำนวนของรูปแบบ

ค่า = (-1) ^ s * (1.m 51ม. 50 ... ม. 2ม. 1ม. 0 ) 2 * 2 e-1023

ใน 64 บิต:

  • บิตแรกคือบิตเครื่องหมาย : 1ถ้าตัวเลขเป็นลบ0มิฉะนั้น1 1
  • 11 บิตถัดไปคือเลขชี้กำลังซึ่งถูกชดเชยด้วย 1023 ในคำอื่น ๆ หลังจากอ่านบิตเลขชี้กำลังจากจำนวนความแม่นยำสองเท่าแล้ว 1023 จะต้องถูกลบออกเพื่อให้ได้พลังงานของทั้งสอง
  • ส่วนที่เหลืออีก 52 บิตเป็นซิก (หรือ mantissa) ใน mantissa ซึ่งเป็น 'นัย' 1.อยู่เสมอ2ละเว้นตั้งแต่บิตที่สำคัญที่สุดของค่าไบนารีใด ๆ 1ที่เป็น

1 - IEEE 754 อนุญาตให้มีแนวคิดของศูนย์ที่ได้รับการเซ็นชื่อ - +0และ-0ได้รับการปฏิบัติต่างกัน: 1 / (+0)มีค่าเป็นบวก 1 / (-0)เป็นลบไม่สิ้นสุด สำหรับค่าศูนย์บิต mantissa และเลขชี้กำลังเป็นศูนย์ทั้งหมด หมายเหตุ: ค่าศูนย์ (+0 และ -0) ไม่ได้จัดอยู่ในประเภท denormal 2อย่างชัดเจน 2

2 - นี่ไม่ใช่กรณีของตัวเลข denormalซึ่งมีเลขชี้กำลังออฟเซ็ตเป็นศูนย์ (และโดยนัย0.) ช่วงของตัวเลขที่มีความแม่นยำสองเท่าของค่าผิดปกติคือ d min ≤ | x | max d maxโดยที่ d min (หมายเลขที่ไม่สามารถแทนค่าได้น้อยที่สุด) คือ 2 -1023 - 51 (≈ 4.94 * 10 -324 ) และ d max (ตัวเลข denormal ที่ใหญ่ที่สุดซึ่ง mantissa ประกอบด้วยทั้งหมด1s) คือ 2 -1023 + 1 - 2 -1023 - 51 (≈ 2.225 * 10 -308 )


เปลี่ยนจำนวนความแม่นยำสองเท่าให้เป็นไบนารี

ผู้แปลงออนไลน์จำนวนมากมีการแปลงเลขทศนิยมที่มีความแม่นยำสองเท่าเป็นไบนารี (เช่นที่binaryconvert.com ) แต่นี่คือตัวอย่างโค้ด C # ที่จะได้รับการแทน IEEE 754 สำหรับตัวเลขที่มีความแม่นยำสองเท่า (ฉันแยกสามส่วนด้วยเครื่องหมายโคลอน:) :

public static string BinaryRepresentation(double value)
{
    long valueInLongType = BitConverter.DoubleToInt64Bits(value);
    string bits = Convert.ToString(valueInLongType, 2);
    string leadingZeros = new string('0', 64 - bits.Length);
    string binaryRepresentation = leadingZeros + bits;

    string sign = binaryRepresentation[0].ToString();
    string exponent = binaryRepresentation.Substring(1, 11);
    string mantissa = binaryRepresentation.Substring(12);

    return string.Format("{0}:{1}:{2}", sign, exponent, mantissa);
}

มาถึงประเด็น: คำถามเดิม

(ข้ามไปด้านล่างสำหรับรุ่น TL; DR)

Cato Johnston (ผู้ถามคำถาม) ถามว่าทำไม 0.1 + 0.2! = 0.3

เขียนด้วยเลขฐานสอง (ที่มีเครื่องหมายทวิภาคคั่นสามส่วน) การแทนค่า IEEE 754 ของค่าคือ:

0.1 => 0:01111111011:1001100110011001100110011001100110011001100110011010
0.2 => 0:01111111100:1001100110011001100110011001100110011001100110011010

โปรดทราบว่า mantissa 0011ที่ประกอบด้วยหลักที่เกิดขึ้น นี่คือกุญแจสำคัญไปทำไมมีข้อผิดพลาดใด ๆ ในการคำนวณ - 0.1, 0.2 และ 0.3 ไม่สามารถแสดงในไบนารีได้อย่างแม่นยำในจำกัดจำนวนบิตไบนารีใด ๆ มากกว่า 1/9, 1/3 หรือ 1/7 สามารถแสดงได้อย่างแม่นยำในเลขทศนิยมตัวเลขทศนิยม

นอกจากนี้โปรดทราบว่าเราสามารถลดพลังงานในเลขชี้กำลังเป็น 52 และเลื่อนจุดในการแทนเลขฐานสองไปทางขวา 52 แห่ง (เช่น 10 -3 * 1.23 == 10 -5 * 123) สิ่งนี้ทำให้เราสามารถแสดงการแทนแบบไบนารี่เป็นค่าที่แน่นอนที่มันแสดงในรูปแบบ * 2 pพีโดยที่ 'a' เป็นจำนวนเต็ม

การแปลงเลขชี้กำลังเป็นทศนิยมนำการชดเชยออกและเพิ่มการบอกเป็นนัย1(ในวงเล็บเหลี่ยม), 0.1 และ 0.2 คือ:

0.1 => 2^-4 * [1].1001100110011001100110011001100110011001100110011010
0.2 => 2^-3 * [1].1001100110011001100110011001100110011001100110011010
or
0.1 => 2^-56 * 7205759403792794 = 0.1000000000000000055511151231257827021181583404541015625
0.2 => 2^-55 * 7205759403792794 = 0.200000000000000011102230246251565404236316680908203125

ในการเพิ่มตัวเลขสองตัวเลขชี้กำลังจำเป็นต้องเหมือนกันเช่น:

0.1 => 2^-3 *  0.1100110011001100110011001100110011001100110011001101(0)
0.2 => 2^-3 *  1.1001100110011001100110011001100110011001100110011010
sum =  2^-3 * 10.0110011001100110011001100110011001100110011001100111
or
0.1 => 2^-55 * 3602879701896397  = 0.1000000000000000055511151231257827021181583404541015625
0.2 => 2^-55 * 7205759403792794  = 0.200000000000000011102230246251565404236316680908203125
sum =  2^-55 * 10808639105689191 = 0.3000000000000000166533453693773481063544750213623046875

เนื่องจากผลรวมไม่ได้อยู่ในรูปแบบ 2 n * 1 {bbb} เราเพิ่มเลขยกกำลังหนึ่งและเลื่อนจุดทศนิยม ( ไบนารี ) เพื่อให้ได้รับ:

sum = 2^-2  * 1.0011001100110011001100110011001100110011001100110011(1)
    = 2^-54 * 5404319552844595.5 = 0.3000000000000000166533453693773481063544750213623046875

ขณะนี้มี 53 บิตในแมนทิสซา (53rd อยู่ในวงเล็บเหลี่ยมในบรรทัดด้านบน) โหมดการปัดเศษเริ่มต้นสำหรับ IEEE 754 คือ ' Round to Near ' - เช่นถ้าตัวเลขxอยู่ระหว่างสองค่าaและbค่าที่เลือกบิตที่สำคัญน้อยที่สุดคือศูนย์

a = 2^-54 * 5404319552844595 = 0.299999999999999988897769753748434595763683319091796875
  = 2^-2  * 1.0011001100110011001100110011001100110011001100110011

x = 2^-2  * 1.0011001100110011001100110011001100110011001100110011(1)

b = 2^-2  * 1.0011001100110011001100110011001100110011001100110100
  = 2^-54 * 5404319552844596 = 0.3000000000000000444089209850062616169452667236328125

โปรดทราบว่าaและbแตกต่างกันในบิตสุดท้ายเท่านั้น ...0011+ =1 ...0100ในกรณีนี้ค่าที่มีบิตที่มีนัยสำคัญน้อยที่สุดคือbดังนั้นผลรวมคือ:

sum = 2^-2  * 1.0011001100110011001100110011001100110011001100110100
    = 2^-54 * 5404319552844596 = 0.3000000000000000444089209850062616169452667236328125

ในขณะที่การเป็นตัวแทนไบนารีของ 0.3 คือ:

0.3 => 2^-2  * 1.0011001100110011001100110011001100110011001100110011
    =  2^-54 * 5404319552844595 = 0.299999999999999988897769753748434595763683319091796875

ซึ่งแตกต่างจากฐานเป็นตัวแทนของผลรวมของ 0.1 และ 0.2 โดย 2 -54

การแทนเลขฐานสองของ 0.1 และ 0.2 เป็นการนำเสนอที่แม่นยำที่สุดของตัวเลขที่ IEEE 754 อนุญาตการเพิ่มการแทนเหล่านี้เนื่องจากโหมดการปัดเศษเริ่มต้นส่งผลให้ค่าที่แตกต่างในบิตที่มีนัยสำคัญน้อยที่สุด

TL; DR

เขียน0.1 + 0.2ในการเป็นตัวแทนไบนารี IEEE 754 (ด้วย colons แยกสามส่วน) และเปรียบเทียบกับ0.3นี่คือ (ฉันได้ใส่บิตที่แตกต่างในวงเล็บเหลี่ยม):

0.1 + 0.2 => 0:01111111101:0011001100110011001100110011001100110011001100110[100]
0.3       => 0:01111111101:0011001100110011001100110011001100110011001100110[011]

แปลงกลับเป็นทศนิยมค่าเหล่านี้คือ:

0.1 + 0.2 => 0.300000000000000044408920985006...
0.3       => 0.299999999999999988897769753748...

ความแตกต่างคือ 2 -54ซึ่งเป็น ~ 5.5511151231258 × 10 -17 - ไม่สำคัญ (สำหรับการใช้งานจำนวนมาก) เมื่อเปรียบเทียบกับค่าดั้งเดิม

การเปรียบเทียบจำนวนบิตสุดท้ายของจำนวนจุดลอยตัวนั้นเป็นอันตรายโดยธรรมชาติเพราะใครก็ตามที่อ่านชื่อ " สิ่งที่นักวิทยาศาสตร์คอมพิวเตอร์ทุกคนควรรู้เกี่ยวกับเลขทศนิยมนั้น" (ซึ่งครอบคลุมส่วนสำคัญทั้งหมดของคำตอบนี้) จะรู้

เครื่องคิดเลขส่วนใหญ่ใช้หน่วยป้องกันเพิ่มเติมเพื่อแก้ไขปัญหานี้ซึ่งเป็นวิธีที่0.1 + 0.2จะให้0.3: บิตสุดท้ายจะถูกปัดเศษ


14
คำตอบของฉันลงคะแนนในไม่ช้าหลังจากโพสต์ ฉันได้ทำการเปลี่ยนแปลงหลายครั้ง (รวมถึงการสังเกตบิตที่เกิดขึ้นอย่างชัดเจนเมื่อเขียน 0.1 และ 0.2 ในไบนารีซึ่งฉันไม่ได้ใส่ไว้ในต้นฉบับ) ในโอกาสที่ผู้ลงคะแนนเสียงเห็นสิ่งนี้คุณช่วยกรุณาให้ข้อเสนอแนะเพื่อให้ฉันปรับปรุงคำตอบของฉันได้ไหม? ฉันรู้สึกว่าคำตอบของฉันเพิ่มสิ่งใหม่ ๆ ตั้งแต่การรักษาผลรวมใน IEEE 754 ไม่ครอบคลุมในแบบเดียวกับคำตอบอื่น ๆ ในขณะที่ "สิ่งที่นักวิทยาศาสตร์คอมพิวเตอร์ทุกคนควรรู้ ... " ครอบคลุมเนื้อหาเดียวกันบางคำตอบของฉันเกี่ยวข้องเฉพาะกับกรณีของ 0.1 + 0.2
Wai Ha Lee

57

ตัวเลขทศนิยมที่เก็บไว้ในคอมพิวเตอร์ประกอบด้วยสองส่วนคือจำนวนเต็มและเลขชี้กำลังที่ฐานถูกนำไปใช้และคูณด้วยส่วนจำนวนเต็ม

ถ้าคอมพิวเตอร์ที่กำลังทำงานอยู่ในฐาน 10 0.1จะ1 x 10⁻¹, 0.2จะเป็น2 x 10⁻¹และจะเป็น0.3 3 x 10⁻¹คณิตศาสตร์จำนวนเต็มเป็นเรื่องง่ายและที่แน่นอนเพื่อเพิ่มอย่างเห็นได้ชัดจะส่งผลให้0.1 + 0.20.3

คอมพิวเตอร์มักจะไม่ได้ทำงานในฐานที่ 10 พวกเขาทำงานในฐาน 2. คุณยังสามารถได้รับผลที่แน่นอนสำหรับค่าบางอย่างเช่น0.5เป็น1 x 2⁻¹และ0.25เป็น1 x 2⁻²และเพิ่มพวกเขาส่งผลให้ในหรือ3 x 2⁻² 0.75เผง

ปัญหามาพร้อมกับตัวเลขที่สามารถแสดงได้อย่างแน่นอนในฐาน 10 แต่ไม่ได้อยู่ในฐาน 2 ตัวเลขเหล่านั้นต้องถูกปัดเศษให้ใกล้เคียงกับค่าที่ใกล้เคียงที่สุด สมมติว่า IEEE 64 บิตรูปแบบที่พบบ่อยมากลอยจุดจำนวนที่อยู่ใกล้0.1เป็น3602879701896397 x 2⁻⁵⁵และจำนวนที่อยู่ใกล้0.2คือ7205759403792794 x 2⁻⁵⁵; เพิ่มพวกเขาร่วมกันส่งผลในหรือค่าทศนิยมที่แน่นอนของ10808639105689191 x 2⁻⁵⁵ 0.3000000000000000444089209850062616169452667236328125หมายเลขจุดลอยตัวจะถูกปัดเศษเพื่อแสดงผล


2
@ Mark ขอบคุณสำหรับคำอธิบายที่ชัดเจนนี้ แต่คำถามนั้นเกิดขึ้นเพราะเหตุใด 0.1 + 0.4 จึงเพิ่มขึ้นอย่างมากถึง 0.5 (atleast ใน Python 3) เป็นวิธีที่ดีที่สุดในการตรวจสอบความเท่าเทียมกันเมื่อใช้ลอยใน Python 3 อะไร
pchegoor

2
@ user2417881 การดำเนินการจุดลอย IEEE มีกฎการปัดเศษสำหรับทุกการดำเนินการและบางครั้งการปัดเศษสามารถสร้างคำตอบที่แน่นอนแม้ว่าตัวเลขทั้งสองจะถูกปิดเล็กน้อย รายละเอียดยาวเกินไปสำหรับความคิดเห็นและฉันไม่ใช่ผู้เชี่ยวชาญในเรื่องนั้น ดังที่คุณเห็นในคำตอบนี้ 0.5 เป็นหนึ่งในไม่กี่ทศนิยมที่สามารถแสดงเป็นเลขฐานสอง แต่นั่นเป็นเรื่องบังเอิญ เพื่อความเท่าเทียมกันทดสอบดูstackoverflow.com/questions/5595425/...
Mark Ransom

1
@ user2417881 คำถามของคุณทำให้ฉันทึ่งฉันจึงเปลี่ยนเป็นคำถามและคำตอบแบบเต็ม: stackoverflow.com/q/48374522/5987
Mark Ransom

47

ข้อผิดพลาดในการปัดเศษทศนิยม จากสิ่งที่นักวิทยาศาสตร์คอมพิวเตอร์ทุกคนควรรู้เกี่ยวกับเลขคณิตทศนิยม :

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


33

วิธีแก้ปัญหาของฉัน:

function add(a, b, precision) {
    var x = Math.pow(10, precision || 2);
    return (Math.round(a * x) + Math.round(b * x)) / x;
}

ความแม่นยำหมายถึงจำนวนหลักที่คุณต้องการรักษาหลังจุดทศนิยมระหว่างการเพิ่ม


30

มีคำตอบที่ดีมากมายถูกโพสต์ แต่ฉันต้องการเพิ่มอีกหนึ่งรายการ

ไม่สามารถแสดงตัวเลขทั้งหมดได้ด้วยการลอยตัว / เป็นสองเท่า ตัวอย่างเช่นหมายเลข "0.2" จะแสดงเป็น "0.200000003" ด้วยความแม่นยำเดียวในมาตรฐาน IEEE754

แบบจำลองสำหรับเก็บตัวเลขจริงภายใต้ประทุนแสดงถึงจำนวนลอยตัวเช่น

ป้อนคำอธิบายรูปภาพที่นี่

แม้ว่าคุณสามารถพิมพ์0.2ได้อย่างง่ายดายFLT_RADIXและDBL_RADIXเป็น 2; ไม่ใช่ 10 สำหรับคอมพิวเตอร์ที่มี FPU ซึ่งใช้ "มาตรฐาน IEEE สำหรับเลขฐานสองทศนิยม (ISO / IEEE Std 754-1985)"

ดังนั้นมันจึงยากที่จะเป็นตัวแทนของตัวเลขเหล่านี้อย่างแน่นอน แม้ว่าคุณจะระบุตัวแปรนี้อย่างชัดเจนโดยไม่มีการคำนวณระดับกลางใด ๆ


28

สถิติบางอย่างเกี่ยวข้องกับคำถามความแม่นยำสองเท่าที่มีชื่อเสียงนี้

เมื่อมีการเพิ่มค่าทั้งหมด ( A + B ) โดยใช้ขั้นตอนที่ 0.1 (0.1-100) เรามี~ โอกาส 15% ของข้อผิดพลาดที่มีความแม่นยำ โปรดทราบว่าข้อผิดพลาดอาจทำให้ค่าใหญ่ขึ้นหรือเล็กลงเล็กน้อย นี่คือตัวอย่างบางส่วน:

0.1 + 0.2 = 0.30000000000000004 (BIGGER)
0.1 + 0.7 = 0.7999999999999999 (SMALLER)
...
1.7 + 1.9 = 3.5999999999999996 (SMALLER)
1.7 + 2.2 = 3.9000000000000004 (BIGGER)
...
3.2 + 3.6 = 6.800000000000001 (BIGGER)
3.2 + 4.4 = 7.6000000000000005 (BIGGER)

เมื่อหักค่าทั้งหมด ( a - bที่A> B ) โดยใช้ขั้นตอนที่ 0.1 (100-0.1) เรามี~ โอกาส 34% ของข้อผิดพลาดที่มีความแม่นยำ นี่คือตัวอย่างบางส่วน:

0.6 - 0.2 = 0.39999999999999997 (SMALLER)
0.5 - 0.4 = 0.09999999999999998 (SMALLER)
...
2.1 - 0.2 = 1.9000000000000001 (BIGGER)
2.0 - 1.9 = 0.10000000000000009 (BIGGER)
...
100 - 99.9 = 0.09999999999999432 (SMALLER)
100 - 99.8 = 0.20000000000000284 (BIGGER)

* 15% และ 34% นั้นใหญ่มากดังนั้นให้ใช้ BigDecimal เสมอเมื่อความแม่นยำมีความสำคัญมาก ด้วยตัวเลขทศนิยม 2 หลัก (ขั้นตอน 0.01) สถานการณ์ยิ่งแย่ลงอีกเล็กน้อย (18% และ 36%)


28

ไม่ไม่แตกหัก แต่เศษส่วนทศนิยมส่วนใหญ่จะต้องประมาณ

สรุป

เลขคณิตของทศนิยมนั้นแน่นอน แต่น่าเสียดายที่มันไม่เข้ากันได้ดีกับการแทนเลขฐาน -10 ของเราดังนั้นมันกลับกลายเป็นว่าเรามักจะให้อินพุตที่แตกต่างจากที่เราเขียนเล็กน้อย

แม้แต่ตัวเลขอย่างง่ายเช่น 0.01, 0.02, 0.03, 0.04 ... 0.24 ไม่สามารถแทนได้ว่าเป็นเศษส่วนแบบไบนารี ถ้าคุณนับขึ้น 0.01, 0.02, 0.03 ... , ไม่ได้จนกว่าคุณจะได้รับ 0.25 คุณจะได้รับส่วนซึ่งแสดงเป็นครั้งแรกในฐาน2 หากคุณลองใช้การใช้ FP คุณจะถูกปิดเล็กน้อย 0.01 ดังนั้นวิธีเดียวที่จะเพิ่มพวกเขา 25 คนในจำนวนที่ดี 0.25 จะต้องมีสายโซ่เหตุที่เกี่ยวข้องกับบิตยามและการปัดเศษ มันยากที่จะคาดเดาดังนั้นเราจึงจับมือกันและพูดว่า"FP นั้นไม่แน่นอน"แต่นั่นไม่จริงเลย

เราให้สิ่งที่ฮาร์ดแวร์ FP อย่างต่อเนื่องที่ดูเหมือนง่ายในฐาน 10 แต่เป็นเศษส่วนซ้ำในฐาน 2

มันเกิดขึ้นได้อย่างไร?

เมื่อเราเขียนเป็นทศนิยมทุกเศษส่วน (โดยเฉพาะทุกทศนิยมสิ้นสุด)เป็นจำนวนตรรกยะของแบบฟอร์ม

           a / (2 n x 5 m )

ในไบนารี่เราได้แค่เทอม2 nนั่นคือ:

           a / 2 n

ดังนั้นในทศนิยมเราไม่สามารถเป็นตัวแทน1 / 3 เนื่องจากฐาน 10 รวม 2 เป็นปัจจัยหลักทุก ๆ จำนวนที่เราสามารถเขียนเป็นเศษส่วนไบนารีได้จึงสามารถเขียนเป็นฐาน 10 ได้ อย่างไรก็ตามแทบจะทุกอย่างที่เราเขียนเป็นเศษส่วนฐาน10เป็นตัวแทนในไบนารี ในช่วง 0.01, 0.02, 0.03 ... 0.99 มีเพียงสามตัวเลขเท่านั้นที่สามารถแสดงในรูปแบบ FP ของเรา: 0.25, 0.50 และ 0.75 เนื่องจากเป็น 1/4, 1/2 และ 3/4 ตัวเลขทั้งหมด มีปัจจัยสำคัญโดยใช้เพียง 2 nระยะ

ในฐาน10เราไม่สามารถเป็นตัวแทน1 / 3 แต่ในไบนารีเราไม่สามารถทำ1 / 10 หรือ 1 / 3

ดังนั้นในขณะที่เศษส่วนไบนารีทุกตัวสามารถเขียนเป็นทศนิยมได้ แต่กลับไม่เป็นความจริง และในความเป็นจริงเศษส่วนทศนิยมส่วนใหญ่ทำซ้ำในไบนารี

จัดการกับมัน

นักพัฒนามักจะได้รับคำแนะนำให้ทำการเปรียบเทียบ<epsilonคำแนะนำที่ดีกว่าอาจเป็นการปัดเศษเป็นค่าอินทิกรัล (ในไลบรารี C: round () และ roundf () เช่นอยู่ในรูปแบบ FP) จากนั้นเปรียบเทียบ การปัดเศษเป็นเศษส่วนทศนิยมเฉพาะจะช่วยแก้ปัญหาส่วนใหญ่ที่เกิดกับเอาต์พุต

นอกจากนี้ปัญหาที่เกิดขึ้นจริงจำนวนมาก (ปัญหาที่ FP ได้คิดค้นขึ้นสำหรับคอมพิวเตอร์ยุคแรกที่มีราคาแพงน่ากลัว) ค่าคงที่ทางกายภาพของจักรวาลและการวัดอื่น ๆ ทั้งหมดเป็นที่ทราบกันว่ามีตัวเลขสำคัญเพียงเล็กน้อยเท่านั้นดังนั้นพื้นที่ปัญหาทั้งหมด คือ "ไม่แน่นอน" อย่างไรก็ตาม FP "ความแม่นยำ" ไม่ใช่ปัญหาในแอปพลิเคชันประเภทนี้

ปัญหาทั้งหมดเกิดขึ้นจริง ๆ เมื่อผู้คนพยายามใช้ FP สำหรับนับถั่ว มันใช้งานได้ แต่ถ้าคุณยึดติดกับค่าที่เป็นส่วนประกอบซึ่งเป็นจุดที่เอาชนะมันได้ นี่คือเหตุผลที่เรามีไลบรารี่เลขทศนิยมทั้งหมด

ฉันรักพิซซ่าตอบโดยคริสเพราะมันอธิบายถึงปัญหาที่เกิดขึ้นจริงไม่ใช่เพียงแค่การพูดคุยเกี่ยวกับ "ความไม่ถูกต้อง" หาก FP เป็นเพียง "ไม่ถูกต้อง" เราสามารถแก้ไขได้และจะทำเสร็จหลายสิบปีที่แล้ว เหตุผลที่เราไม่ได้เป็นเพราะรูปแบบ FP นั้นกะทัดรัดและรวดเร็วและเป็นวิธีที่ดีที่สุดในการบีบตัวเลขจำนวนมาก นอกจากนี้ยังเป็นมรดกจากยุคอวกาศและการแข่งขันทางด้านอาวุธและความพยายามในช่วงต้นเพื่อแก้ไขปัญหาใหญ่กับคอมพิวเตอร์ที่ช้ามากโดยใช้ระบบหน่วยความจำขนาดเล็ก (บางครั้งแกนแม่เหล็กแต่ละอันสำหรับการจัดเก็บ 1 บิต แต่นั่นเป็นอีกเรื่องหนึ่ง )

ข้อสรุป

หากคุณเป็นเพียงแค่นับถั่วที่ธนาคารโซลูชั่นซอฟต์แวร์ที่ใช้การแสดงสตริงทศนิยมในสถานที่แรกที่ทำงานได้ดีอย่างสมบูรณ์ แต่คุณไม่สามารถทำควอนตัม Chromodynamics หรืออากาศพลศาสตร์แบบนั้นได้


การปัดเศษเป็นจำนวนเต็มที่ใกล้ที่สุดไม่ใช่วิธีที่ปลอดภัยในการแก้ปัญหาการเปรียบเทียบในทุกกรณี 0.4999998 และ 0.500001 ปัดเศษเป็นจำนวนเต็มที่แตกต่างกันดังนั้นจึงมี "เขตอันตราย" รอบจุดตัดทุกจุด (ฉันรู้ว่าผู้สตริงทศนิยมอาจจะไม่ตรงกับที่แทนได้ IEEE ลอยไบนารี.)
ปีเตอร์ Cordes

1
แม้ว่าจุดลอยตัวจะเป็นรูปแบบ "ดั้งเดิม" แต่ก็ถูกออกแบบมาอย่างดี ฉันไม่รู้อะไรเลยว่าจะมีใครเปลี่ยนแปลงถ้าออกแบบใหม่ในตอนนี้ ยิ่งฉันเรียนรู้มากเท่าไหร่ฉันก็ยิ่งคิดว่ามันออกแบบมาได้ดี เช่นเลขชี้กำลัง biased หมายถึงเลขฐานสองต่อเนื่องกันมีจำนวนเต็มต่อเนื่องดังนั้นคุณสามารถนำไปใช้nextafter()กับการเพิ่มหรือลดจำนวนเต็มในการแทนเลขฐานสองของการลอยแบบ IEEE นอกจากนี้คุณสามารถเปรียบเทียบการลอยเป็นจำนวนเต็มและได้รับคำตอบที่ถูกต้องยกเว้นเมื่อทั้งคู่เป็นค่าลบ (เนื่องจากส่วนประกอบการลงชื่อกับขนาดของ 2)
Peter Cordes

ฉันไม่เห็นด้วยลอยควรเก็บไว้เป็นทศนิยมและไม่ไบนารีและปัญหาทั้งหมดได้รับการแก้ไข
Ronen Festinger

ไม่ควร " x / (2 ^ n + 5 ^ n) " เป็น " x / (2 ^ n * 5 ^ n) "?
Wai Ha Lee

@RonenFestinger - แล้วประมาณ 1/3
สตีเฟนซี

19

คุณลองใช้โซลูชันเทปพันท่อหรือไม่?

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

 if( (n * 0.1) < 100.0 ) { return n * 0.1 - 0.000000000000001 ;}
                    else { return n * 0.1 + 0.000000000000001 ;}    

ฉันมีปัญหาเดียวกันในโครงการจำลองทางวิทยาศาสตร์ใน c # และฉันสามารถบอกคุณได้ว่าถ้าคุณไม่สนใจเอฟเฟกต์ของผีเสื้อมันจะกลายเป็นมังกรอ้วนตัวโตและกัดคุณใน **


19

เพื่อที่จะเสนอทางออกที่ดีที่สุดฉันสามารถพูดได้ว่าฉันค้นพบวิธีการดังต่อไปนี้:

parseFloat((0.1 + 0.2).toFixed(10)) => Will return 0.3

ให้ฉันอธิบายว่าทำไมมันจึงเป็นทางออกที่ดีที่สุด ตามที่คนอื่น ๆ ที่กล่าวถึงข้างต้นคำตอบมันเป็นความคิดที่ดีที่จะใช้พร้อมที่จะใช้ฟังก์ชั่น Javascript toFixed () เพื่อแก้ปัญหา แต่ส่วนใหญ่คุณจะพบกับปัญหาบางอย่าง

ลองนึกภาพคุณกำลังจะเพิ่มขึ้นสองหมายเลขลอยเหมือน0.2และนี่คือ:0.70.2 + 0.7 = 0.8999999999999999

ผลลัพธ์ที่คุณคาดหวังคือ0.9หมายความว่าคุณต้องการผลลัพธ์ที่มีความแม่นยำ 1 หลักในกรณีนี้ ดังนั้นคุณควรใช้(0.2 + 0.7).tofixed(1) แต่คุณไม่สามารถให้พารามิเตอร์ที่แน่นอนแก่ toFixed () เนื่องจากมันขึ้นอยู่กับจำนวนที่กำหนดตัวอย่างเช่น

`0.22 + 0.7 = 0.9199999999999999`

ในตัวอย่างนี้คุณจำเป็นต้องมีความแม่นยำ 2 หลักtoFixed(2)ดังนั้นควรมีพารามิเตอร์ใดที่จะพอดีกับจำนวนลอยที่กำหนด

คุณอาจพูดว่าปล่อยให้เป็น 10 ในทุกสถานการณ์แล้ว:

(0.2 + 0.7).toFixed(10) => Result will be "0.9000000000"

ประณาม! คุณจะทำอย่างไรกับศูนย์ที่ไม่ต้องการหลังจาก 9 ถึงเวลาแปลงเป็นลอยเพื่อให้เป็นไปตามที่คุณต้องการ:

parseFloat((0.2 + 0.7).toFixed(10)) => Result will be 0.9

ตอนนี้คุณได้พบวิธีแก้ปัญหาแล้วมันจะดีกว่าที่จะให้มันเป็นฟังก์ชั่นเช่นนี้:

function floatify(number){
           return parseFloat((number).toFixed(10));
        }

ลองด้วยตัวเอง:

function floatify(number){
       return parseFloat((number).toFixed(10));
    }
 
function addUp(){
  var number1 = +$("#number1").val();
  var number2 = +$("#number2").val();
  var unexpectedResult = number1 + number2;
  var expectedResult = floatify(number1 + number2);
  $("#unexpectedResult").text(unexpectedResult);
  $("#expectedResult").text(expectedResult);
}
addUp();
input{
  width: 50px;
}
#expectedResult{
color: green;
}
#unexpectedResult{
color: red;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<input id="number1" value="0.2" onclick="addUp()" onkeyup="addUp()"/> +
<input id="number2" value="0.7" onclick="addUp()" onkeyup="addUp()"/> =
<p>Expected Result: <span id="expectedResult"></span></p>
<p>Unexpected Result: <span id="unexpectedResult"></span></p>

คุณสามารถใช้วิธีนี้:

var x = 0.2 + 0.7;
floatify(x);  => Result: 0.9

เนื่องจากW3SCHOOLSแนะนำว่ามีวิธีแก้ไขปัญหาอื่นด้วยคุณสามารถคูณและหารเพื่อแก้ปัญหาข้างต้น:

var x = (0.2 * 10 + 0.1 * 10) / 10;       // x will be 0.3

โปรดทราบว่า(0.2 + 0.1) * 10 / 10จะไม่ทำงานเลยแม้ว่าจะดูเหมือนกัน! ฉันชอบวิธีแก้ปัญหาแรกเพราะฉันสามารถใช้มันเป็นฟังก์ชั่นที่แปลงอินพุตโฟลล์เป็นเอาท์พุทโฟลิโอที่แม่นยำ


ทำให้ฉันปวดหัวจริง ฉันรวม 12 จำนวนลอยแล้วแสดงผลรวมและค่าเฉลี่ยหากตัวเลขเหล่านั้น การใช้ toFixed () อาจแก้ไขผลรวมของ 2 หมายเลข แต่เมื่อรวมจำนวนหลาย ๆ อย่างการก้าวกระโดดมีความสำคัญ
Nuryagdy Mustapayev

@Nuryagdy Mustapayev ฉันไม่ได้รับความตั้งใจของคุณในขณะที่ฉันทดสอบก่อนที่คุณจะสามารถรวม 12 จำนวนทศนิยมแล้วใช้ฟังก์ชั่น floatify () ผลแล้วทำสิ่งที่คุณต้องการฉันสังเกตเห็นว่าไม่มีการใช้มัน
Mohammad Musavi

ฉันแค่บอกว่าในสถานการณ์ของฉันที่ฉันมีพารามิเตอร์ประมาณ 20 และ 20 สูตรที่ผลของแต่ละสูตรขึ้นอยู่กับคนอื่น ๆ โซลูชันนี้ไม่ได้ช่วย
Nuryagdy Mustapayev

16

ตัวเลขแปลก ๆ เหล่านั้นปรากฏขึ้นเนื่องจากคอมพิวเตอร์ใช้ระบบเลขฐานสอง (ฐาน 2) เพื่อการคำนวณในขณะที่เราใช้ทศนิยม (ฐาน 10)

มีตัวเลขเศษส่วนส่วนใหญ่ที่ไม่สามารถแสดงได้อย่างแม่นยำทั้งในไบนารีหรือทศนิยมหรือทั้งสองอย่าง ผลลัพธ์ - ผลลัพธ์ตัวเลขปัดเศษ (แต่แม่นยำ)


ฉันไม่เข้าใจย่อหน้าที่สองของคุณเลย
เน

1
@ ไม่ฉันจะแปลย่อหน้าที่สองเป็น "เศษส่วนส่วนใหญ่ไม่สามารถแสดงได้อย่างแม่นยำทั้งทศนิยมหรือไบนารีดังนั้นผลลัพธ์ส่วนใหญ่จะถูกปัดเศษ - แม้ว่าพวกเขาจะยังคงแม่นยำกับจำนวนบิต / ตัวเลขโดยธรรมชาติในการเป็นตัวแทน กำลังใช้."
Steve Summit

15

คำถามที่ซ้ำกันจำนวนมากของคำถามนี้ถามเกี่ยวกับผลกระทบของการปัดเศษทศนิยมตามจำนวนที่ระบุ ในทางปฏิบัติมันง่ายกว่าที่จะรับความรู้สึกว่ามันทำงานอย่างไรโดยการดูผลลัพธ์ที่แน่นอนของการคำนวณดอกเบี้ยมากกว่าเพียงแค่อ่านเกี่ยวกับมัน บางภาษามีวิธีการทำเช่น - การแปลง a floatหรือdoubleเป็นBigDecimalJava

เนื่องจากนี่เป็นคำถามเกี่ยวกับผู้ไม่เชื่อเรื่องพระเจ้าจึงจำเป็นต้องมีเครื่องมือที่ไม่เชื่อเรื่องภาษาเช่นDecimal to Floating-Point Converterแปลง

นำไปใช้กับตัวเลขในคำถามโดยถือว่าเป็นสองเท่า:

0.1 แปลงเป็น 0.10000000000000000000055511151231257827021181583404541015625,

0.2 แปลงเป็น 0.200000000000000011102230246251565404236316680908203125,

0.3 แปลงเป็น 0.2999999999999998888777769753748434595763683319091796875 และ

0.30000000000000004 แปลงเป็น 0.3000000000000000444089209850062616169452667236328125

การเพิ่มตัวเลขสองตัวแรกด้วยตนเองหรือในเครื่องคิดเลขทศนิยมเช่นเครื่องคำนวณความแม่นยำเต็มรูปแบบแสดงผลรวมที่แน่นอนของอินพุตที่แท้จริงคือ 0.3000000000000000166533453693773481063544750213623046875

หากมันถูกปัดเศษให้เท่ากับ 0.3 ข้อผิดพลาดในการปัดเศษจะเท่ากับ 0.00000000000000000000277555756156289135105907917022225050125125 การปัดเศษขึ้นให้เทียบเท่า 0.30000000000000004 ยังให้ข้อผิดพลาดในการปัดเศษ 0.0000000000000000277555756156289135105907917022705078125 ใช้เบรกเกอร์แบบกลมต่อคู่

กลับไปที่ตัวแปลงจุดลอยตัวเลขฐานสิบหกที่แท้จริงสำหรับ 0.30000000000000004 คือ 3fd3333333333334 ซึ่งลงท้ายด้วยเลขคู่ดังนั้นจึงเป็นผลลัพธ์ที่ถูกต้อง


2
สำหรับผู้ที่มีการแก้ไขฉันเพิ่งย้อนกลับ: ฉันพิจารณาราคาโค้ดที่เหมาะสมสำหรับการอ้างอิงรหัส คำตอบนี้เป็นภาษากลางไม่มีรหัสที่ยกมาใด ๆ ตัวเลขสามารถใช้ในประโยคภาษาอังกฤษและไม่เปลี่ยนเป็นรหัส
Patricia Shanahan

นี่อาจเป็นสาเหตุที่มีคนจัดรูปแบบหมายเลขของคุณเป็นรหัสไม่ใช่สำหรับการจัดรูปแบบ แต่เพื่อความสะดวกในการอ่าน
ไหวฮาลี

... ยังการปัดเศษเพื่ออ้างถึงการแทนฐานสองไม่ใช่การแทนทศนิยม ดูนี้หรือเช่นนี้
ไหวฮาลี

@WaiHaLee ฉันไม่ได้ใช้การทดสอบคี่ / คู่กับตัวเลขทศนิยมใด ๆ ที่เป็นเลขฐานสิบหกเท่านั้น เลขฐานสิบหกหลักคือแม้ว่าและหากบิตที่สำคัญน้อยที่สุดของการขยายฐานสองของมันคือศูนย์
Patricia Shanahan

14

เนื่องจากไม่มีใครพูดถึงเรื่องนี้ ...

ภาษาระดับสูงบางภาษาเช่น Python และ Java มาพร้อมกับเครื่องมือในการเอาชนะข้อ จำกัด เลขทศนิยมแบบไบนารี ตัวอย่างเช่น:

  • decimalโมดูลของ Python และBigDecimalคลาสของ Java ที่แสดงตัวเลขภายในด้วยเครื่องหมายทศนิยม (ตรงข้ามกับสัญกรณ์ไบนารี) ทั้งสองมีความแม่นยำที่ จำกัด ดังนั้นพวกเขาจึงยังคงมีข้อผิดพลาดได้ง่าย แต่พวกเขาแก้ปัญหาที่พบบ่อยที่สุดกับเลขคณิตทศนิยมเลขฐานสอง

    ทศนิยมที่ดีมากเมื่อจัดการกับเงิน: สิบเซ็นต์บวกยี่สิบเซ็นต์อยู่เสมอว่าสามสิบเซ็นต์:

    >>> 0.1 + 0.2 == 0.3
    False
    >>> Decimal('0.1') + Decimal('0.2') == Decimal('0.3')
    True
    

    decimalโมดูลจะขึ้นอยู่กับมาตรฐาน IEEE 854-1987

  • fractionsโมดูลและ Apache ทั่วไปของชั้นเรียนBigFraction ทั้งสองเป็นตัวแทนของจำนวนตรรกยะเป็น(numerator, denominator)คู่และพวกเขาอาจให้ผลลัพธ์ที่แม่นยำกว่าเลขทศนิยมทศนิยม

วิธีแก้ปัญหาเหล่านี้ไม่สมบูรณ์แบบ (โดยเฉพาะถ้าเราดูการแสดงหรือถ้าเราต้องการความแม่นยำสูงมาก) แต่ก็ยังแก้ปัญหาจำนวนมากด้วยเลขฐานสองทศนิยม


14

ฉันสามารถเพิ่ม; คนมักจะคิดว่านี่เป็นปัญหาคอมพิวเตอร์ แต่ถ้าคุณนับด้วยมือของคุณ (ฐาน 10) คุณจะไม่ได้รับ(1/3+1/3=2/3)=trueจนกว่าคุณจะมีอินฟินิตี้เพิ่ม 0.333 ... ถึง 0.333 ... เช่นเดียวกับ(1/10+2/10)!==3/10ปัญหาในฐาน 2 คุณตัดทอนเป็น 0.333 + 0.333 = 0.666 และอาจปัดเป็น 0.667 ซึ่งก็จะไม่ถูกต้องทางเทคนิคเช่นกัน

นับเป็นสามส่วนและสามในนั้นไม่ใช่ปัญหา - บางทีการแข่งขันที่มี 15 นิ้วในแต่ละมือจะถามว่าทำไมคณิตศาสตร์ทศนิยมของคุณถึงแตก ...


เนื่องจากมนุษย์ใช้ตัวเลขทศนิยมฉันไม่เห็นเหตุผลที่ดีว่าเหตุใดการลอยไม่แสดงเป็นทศนิยมตามค่าเริ่มต้นดังนั้นเราจึงได้ผลลัพธ์ที่แม่นยำ
Ronen Festinger

มนุษย์ใช้ฐานจำนวนมากนอกเหนือจากฐาน 10 (ทศนิยม) เลขฐานสองเป็นฐานที่เราใช้มากที่สุดในการคำนวณ .. 'เหตุผลที่ดี' คือคุณไม่สามารถแสดงเศษส่วนทุกส่วนในทุกฐานได้

@RonenFestinger เลขคณิตไบนารีเป็นเรื่องง่ายที่จะนำไปใช้กับคอมพิวเตอร์เพราะมันต้องการเพียงแปดการดำเนินงานขั้นพื้นฐานที่มีตัวเลข: พูด $ a $, $ b $ ใน $ 0,1 $ ทั้งหมดที่คุณต้องรู้คือ $ \ operatorname {xor} (a, b) $ และ $ \ operatorname {cb} (a, b) $, โดยที่ xor เป็นเอกสิทธิ์หรือ cb คือ "carry bit" ซึ่งเป็น $ 0 $ ในทุกกรณียกเว้นเมื่อ $ a = 1 = b $ ซึ่งในกรณีนี้เรามี หนึ่งรายการ (ในความเป็นจริงการสับเปลี่ยนการดำเนินการทั้งหมดช่วยให้คุณประหยัด $ 2 $ รายและสิ่งที่คุณต้องการคือ $ 6 $ กฎ) การขยายทศนิยมต้องมีกรณี $ 10 \ times 11 $ (ในรูปแบบทศนิยม) และจะถูกเก็บไว้ในสถานะที่แตกต่างกัน $ 10 $ สำหรับแต่ละบิต
Oskar Limka

@RonenFestinger - ทศนิยมไม่แม่นยำมากขึ้น นั่นคือสิ่งที่คำตอบนี้จะพูด สำหรับฐานใด ๆ ที่คุณเลือกจะมีจำนวนตรรกยะ (เศษส่วน) ที่ให้ลำดับซ้ำหลักอย่างไม่สิ้นสุด สำหรับบันทึกที่บางส่วนของคอมพิวเตอร์ครั้งแรกทำฐานการใช้งาน 10 การแสดงสำหรับตัวเลข แต่นักออกแบบฮาร์ดแวร์คอมพิวเตอร์เป็นผู้บุกเบิกในเร็ว ๆ นี้ได้ข้อสรุปว่าฐาน 2 เป็นเรื่องง่ายและมีประสิทธิภาพมากขึ้นในการดำเนินการ
สตีเฟ่นซี

9

ชนิดของเลขทศนิยมที่สามารถนำมาใช้ในคอมพิวเตอร์ดิจิทัลจำเป็นต้องใช้การประมาณจำนวนจริงและการดำเนินการกับพวกเขา ( รุ่นมาตรฐานทำงานได้มากกว่าห้าสิบหน้าของเอกสารและมีคณะกรรมการเพื่อจัดการกับ errata และการปรับแต่งเพิ่มเติม)

การประมาณนี้เป็นส่วนผสมของการประมาณค่าของชนิดต่าง ๆ ซึ่งแต่ละอย่างสามารถละเว้นหรือคิดอย่างรอบคอบเนื่องจากลักษณะเฉพาะของการเบี่ยงเบนจากความแน่นอน นอกจากนี้ยังเกี่ยวข้องกับกรณีพิเศษที่ชัดเจนทั้งในระดับฮาร์ดแวร์และซอฟต์แวร์ที่คนส่วนใหญ่เดินผ่านมาในขณะที่แสร้งทำเป็นไม่สังเกตเห็น

หากคุณต้องการความแม่นยำที่ไม่สิ้นสุด (โดยใช้หมายเลข instead ตัวอย่างเช่นแทนที่จะเป็นหนึ่งในสแตนเลสที่สั้นกว่า) คุณควรเขียนหรือใช้โปรแกรมคณิตศาสตร์สัญลักษณ์แทน

แต่ถ้าคุณไม่เป็นไรกับความคิดที่ว่าบางครั้งคณิตศาสตร์แบบลอยตัวนั้นคลุมเครือในค่าและตรรกะและข้อผิดพลาดสามารถสะสมได้อย่างรวดเร็วและคุณสามารถเขียนข้อกำหนดและการทดสอบของคุณเพื่ออนุญาตสิ่งนั้นได้ FPU ของคุณ


9

เพื่อความสนุกฉันเล่นกับการแสดงลอยตามคำจำกัดความจาก Standard C99 และฉันเขียนโค้ดด้านล่าง

รหัสจะพิมพ์การแทนแบบไบนารีของการลอยในกลุ่มที่แยก 3 กลุ่ม

SIGN EXPONENT FRACTION

และหลังจากนั้นมันจะพิมพ์ผลรวมว่าเมื่อรวมกับความแม่นยำที่เพียงพอแล้วมันจะแสดงค่าที่มีอยู่จริงในฮาร์ดแวร์

ดังนั้นเมื่อคุณเขียนfloat x = 999...คอมไพเลอร์จะแปลงตัวเลขนั้นในรูปแบบบิตที่พิมพ์โดยฟังก์ชันxxเช่นผลรวมที่พิมพ์โดยฟังก์ชันyyนั้นเท่ากับจำนวนที่กำหนด

ในความเป็นจริงผลรวมนี้เป็นเพียงการประมาณ สำหรับหมายเลข 999,999,999 คอมไพเลอร์จะแทรกบิตแทนการลอยจำนวน 1,000,000,000

หลังจากรหัสฉันแนบคอนโซลเซสชันซึ่งฉันคำนวณผลรวมของเงื่อนไขสำหรับค่าคงที่ทั้งสอง (ลบ PI และ 999999999) ที่มีอยู่จริงในฮาร์ดแวร์แทรกโดยคอมไพเลอร์

#include <stdio.h>
#include <limits.h>

void
xx(float *x)
{
    unsigned char i = sizeof(*x)*CHAR_BIT-1;
    do {
        switch (i) {
        case 31:
             printf("sign:");
             break;
        case 30:
             printf("exponent:");
             break;
        case 23:
             printf("fraction:");
             break;

        }
        char b=(*(unsigned long long*)x&((unsigned long long)1<<i))!=0;
        printf("%d ", b);
    } while (i--);
    printf("\n");
}

void
yy(float a)
{
    int sign=!(*(unsigned long long*)&a&((unsigned long long)1<<31));
    int fraction = ((1<<23)-1)&(*(int*)&a);
    int exponent = (255&((*(int*)&a)>>23))-127;

    printf(sign?"positive" " ( 1+":"negative" " ( 1+");
    unsigned int i = 1<<22;
    unsigned int j = 1;
    do {
        char b=(fraction&i)!=0;
        b&&(printf("1/(%d) %c", 1<<j, (fraction&(i-1))?'+':')' ), 0);
    } while (j++, i>>=1);

    printf("*2^%d", exponent);
    printf("\n");
}

void
main()
{
    float x=-3.14;
    float y=999999999;
    printf("%lu\n", sizeof(x));
    xx(&x);
    xx(&y);
    yy(x);
    yy(y);
}

นี่คือเซสชันคอนโซลที่ฉันคำนวณมูลค่าที่แท้จริงของโฟลตที่มีอยู่ในฮาร์ดแวร์ ฉันเคยbcพิมพ์ผลรวมของคำที่ออกโดยโปรแกรมหลัก หนึ่งสามารถแทรกผลรวมนั้นในหลามreplหรือสิ่งที่คล้ายกัน

-- .../terra1/stub
@ qemacs f.c
-- .../terra1/stub
@ gcc f.c
-- .../terra1/stub
@ ./a.out
sign:1 exponent:1 0 0 0 0 0 0 fraction:0 1 0 0 1 0 0 0 1 1 1 1 0 1 0 1 1 1 0 0 0 0 1 1
sign:0 exponent:1 0 0 1 1 1 0 fraction:0 1 1 0 1 1 1 0 0 1 1 0 1 0 1 1 0 0 1 0 1 0 0 0
negative ( 1+1/(2) +1/(16) +1/(256) +1/(512) +1/(1024) +1/(2048) +1/(8192) +1/(32768) +1/(65536) +1/(131072) +1/(4194304) +1/(8388608) )*2^1
positive ( 1+1/(2) +1/(4) +1/(16) +1/(32) +1/(64) +1/(512) +1/(1024) +1/(4096) +1/(16384) +1/(32768) +1/(262144) +1/(1048576) )*2^29
-- .../terra1/stub
@ bc
scale=15
( 1+1/(2) +1/(4) +1/(16) +1/(32) +1/(64) +1/(512) +1/(1024) +1/(4096) +1/(16384) +1/(32768) +1/(262144) +1/(1048576) )*2^29
999999999.999999446351872

แค่นั้นแหละ. ความจริงแล้วมูลค่าของ 999999999 นั้น

999999999.999999446351872

นอกจากนี้คุณยังสามารถตรวจสอบbcด้วยว่า -3.14 นั้นถูกรบกวนด้วยเช่นกัน อย่าลืมที่จะกำหนดปัจจัยในการscalebc

ผลรวมที่ปรากฏคือสิ่งที่อยู่ภายในฮาร์ดแวร์ ค่าที่คุณได้รับจากการคำนวณมันขึ้นอยู่กับขนาดที่คุณตั้งไว้ ฉันตั้งค่าscaleปัจจัยเป็น 15 ในทางคณิตศาสตร์ด้วยความแม่นยำที่ไม่สิ้นสุดดูเหมือนว่าจะเป็น 1,000,000,000


5

อีกวิธีในการดู: ใช้เป็น 64 บิตเพื่อแสดงตัวเลข ดังนั้นจึงไม่มีทางที่มากกว่า 2 ** 64 = 18,446,744,073,709,551,616 ตัวเลขที่แตกต่างกันสามารถแสดงได้อย่างแม่นยำ

อย่างไรก็ตาม Math กล่าวว่ามีทศนิยมจำนวนมากระหว่าง 0 และ 1 IEE 754 กำหนดการเข้ารหัสเพื่อใช้ 64 บิตเหล่านี้อย่างมีประสิทธิภาพสำหรับพื้นที่หมายเลขที่มีขนาดใหญ่กว่าบวก NaN และ +/- Infinity ดังนั้นจึงมีช่องว่างระหว่างตัวเลขที่ถูกต้อง ตัวเลขโดยประมาณเท่านั้น

น่าเสียดายที่ 0.3 อยู่ในช่องว่าง


4

ลองนึกภาพการทำงานในฐานสิบด้วยพูดความแม่นยำ 8 หลัก คุณตรวจสอบว่า

1/3 + 2 / 3 == 1

และเรียนรู้ว่าสิ่งนี้จะกลับfalseมา ทำไม? เช่นเดียวกับตัวเลขจริงที่เรามี

1/3 = 0.333 ....และ2/3 = 0.666 ....

ตัดทอนที่ตำแหน่งทศนิยมแปดตำแหน่งเราได้

0.33333333 + 0.66666666 = 0.99999999

ซึ่งเป็นของหลักสูตรที่แตกต่างจากโดยตรง1.000000000.00000001


สถานการณ์สำหรับเลขฐานสองที่มีจำนวนบิตคงที่นั้นคล้ายคลึงกันทุกประการ ในฐานะที่เป็นตัวเลขจริงเรามี

1/10 = 0.0001100110011001100 ... (ฐาน 2)

และ

1/5 = 0.0011001100110011001 ... (ฐาน 2)

ถ้าเราตัดส่วนเหล่านี้ให้เป็นเจ็ดบิตเราก็จะได้

0.0001100 + 0.0011001 = 0.0100101

ในขณะที่ในอีกทางหนึ่ง

3/10 = 0.01001100110011 ... (ฐาน 2)

ซึ่งตัดออกไปบิตเจ็ดเป็นและสิ่งเหล่านี้แตกต่างกันโดยตรง0.01001100.0000001


สถานการณ์ที่แน่นอนนั้นละเอียดกว่าเล็กน้อยเนื่องจากตัวเลขเหล่านี้มักถูกเก็บไว้ในรูปแบบทางวิทยาศาสตร์ ตัวอย่างเช่นแทนที่จะเก็บ 1/10 เพราะ0.0001100เราอาจเก็บไว้เป็นอย่างอื่น1.10011 * 2^-4ขึ้นอยู่กับจำนวนบิตที่เราจัดสรรสำหรับเลขชี้กำลังและแมนทิสซา สิ่งนี้มีผลต่อจำนวนความแม่นยำที่คุณได้รับสำหรับการคำนวณของคุณ

ผลที่สุดคือเนื่องจากข้อผิดพลาดในการปัดเศษคุณไม่ต้องการใช้ == สำหรับตัวเลขทศนิยม คุณสามารถตรวจสอบว่าค่าสัมบูรณ์ของความแตกต่างนั้นน้อยกว่าจำนวนคงที่บางส่วนแทนหรือไม่



3

เนื่องจากกระทู้นี้แยกออกไปเล็กน้อยในการอภิปรายทั่วไปเกี่ยวกับการใช้งานจุดลอยตัวปัจจุบันฉันต้องการเพิ่มว่ามีโครงการในการแก้ไขปัญหาของพวกเขา

ลองดูที่https://posithub.org/ตัวอย่างเช่นซึ่งแสดงประเภทตัวเลขที่เรียกว่า posit (และ unum บรรพบุรุษของมัน) ที่สัญญาว่าจะให้ความแม่นยำที่ดีขึ้นด้วยบิตที่น้อยลง หากความเข้าใจของฉันถูกต้องก็จะแก้ไขปัญหาในคำถาม ค่อนข้างโครงการที่น่าสนใจคนที่อยู่เบื้องหลังมันเป็นนักคณิตศาสตร์มันดร. จอห์นกุสตาฟ สิ่งทั้งหมดเป็นโอเพนซอร์ซโดยมีการใช้งานจริงใน C / C ++, Python, Julia และ C # ( https://hastlayer.com/arithmetics )


3

จริงๆแล้วมันค่อนข้างง่าย เมื่อคุณมีระบบฐาน 10 (เช่นของเรา) มันสามารถแสดงเศษส่วนที่ใช้ปัจจัยสำคัญของฐานเท่านั้น ปัจจัยสำคัญของ 10 คือ 2 และ 5 ดังนั้น 1/2, 1/4, 1/5, 1/8, และ 1/10 สามารถแสดงออกได้อย่างหมดจดเพราะตัวส่วนทั้งหมดใช้ปัจจัยเฉพาะ 10 ประการในทางตรงกันข้าม 1 / 3, 1/6, และ 1/7 เป็นทศนิยมที่ซ้ำกันทั้งหมดเนื่องจากตัวส่วนของมันใช้ตัวประกอบ 3 หรือ 7 ในเลขฐานสอง (หรือฐาน 2) ตัวประกอบตัวเดียวคือ 2 ดังนั้นคุณสามารถแสดงเศษส่วนได้อย่างหมดจด มีเพียง 2 เป็นปัจจัยสำคัญ ในไบนารี 1/2, 1/4, 1/8 ทั้งหมดจะถูกแสดงอย่างหมดจดเป็นทศนิยม ในขณะที่ 1/5 หรือ 1/10 จะทำซ้ำทศนิยม ดังนั้น 0.1 และ 0.2 (1/10 และ 1/5) ในขณะที่ทำความสะอาดทศนิยมในระบบฐาน 10, กำลังทำซ้ำทศนิยมในระบบฐาน 2 คอมพิวเตอร์ทำงานในเมื่อคุณทำคณิตศาสตร์ในทศนิยมซ้ำเหล่านี้

จากhttps://0.30000000000000004.com/


3

ตัวเลขทศนิยมเช่น0.1, 0.2และ0.3ไม่ได้แสดงว่าในไบนารีเข้ารหัสลอยชนิดจุด ผลรวมของการประมาณ0.1และ0.2แตกต่างจากการประมาณที่ใช้สำหรับ0.3ดังนั้นความเท็จ0.1 + 0.2 == 0.3ที่สามารถมองเห็นได้ชัดเจนยิ่งขึ้นที่นี่:

#include <stdio.h>

int main() {
    printf("0.1 + 0.2 == 0.3 is %s\n", 0.1 + 0.2 == 0.3 ? "true" : "false");
    printf("0.1 is %.23f\n", 0.1);
    printf("0.2 is %.23f\n", 0.2);
    printf("0.1 + 0.2 is %.23f\n", 0.1 + 0.2);
    printf("0.3 is %.23f\n", 0.3);
    printf("0.3 - (0.1 + 0.2) is %g\n", 0.3 - (0.1 + 0.2));
    return 0;
}

เอาท์พุท:

0.1 + 0.2 == 0.3 is false
0.1 is 0.10000000000000000555112
0.2 is 0.20000000000000001110223
0.1 + 0.2 is 0.30000000000000004440892
0.3 is 0.29999999999999998889777
0.3 - (0.1 + 0.2) is -5.55112e-17

สำหรับการคำนวณเหล่านี้จะได้รับการประเมินอย่างน่าเชื่อถือมากขึ้นคุณจะต้องใช้การแสดงทศนิยมตามค่าทศนิยม มาตรฐาน C ไม่ได้ระบุประเภทดังกล่าวตามค่าเริ่มต้น แต่เป็นส่วนขยายที่อธิบายไว้ในรายงานทางเทคนิครายงานทางเทคนิค

_Decimal32, _Decimal64และ_Decimal128ประเภทอาจจะมีอยู่บนระบบของคุณ (เช่นGCCสนับสนุนพวกเขาในการเลือกเป้าหมายแต่เสียงดังกราวไม่สนับสนุนพวกเขาในOS X )


1

Math.sum (javascript) .... ชนิดของการแทนที่โอเปอเรเตอร์

.1 + .0001 + -.1 --> 0.00010000000000000286
Math.sum(.1 , .0001, -.1) --> 0.0001

Object.defineProperties(Math, {
    sign: {
        value: function (x) {
            return x ? x < 0 ? -1 : 1 : 0;
            }
        },
    precision: {
        value: function (value, precision, type) {
            var v = parseFloat(value), 
                p = Math.max(precision, 0) || 0, 
                t = type || 'round';
            return (Math[t](v * Math.pow(10, p)) / Math.pow(10, p)).toFixed(p);
        }
    },
    scientific_to_num: {  // this is from https://gist.github.com/jiggzson
        value: function (num) {
            //if the number is in scientific notation remove it
            if (/e/i.test(num)) {
                var zero = '0',
                        parts = String(num).toLowerCase().split('e'), //split into coeff and exponent
                        e = parts.pop(), //store the exponential part
                        l = Math.abs(e), //get the number of zeros
                        sign = e / l,
                        coeff_array = parts[0].split('.');
                if (sign === -1) {
                    num = zero + '.' + new Array(l).join(zero) + coeff_array.join('');
                } else {
                    var dec = coeff_array[1];
                    if (dec)
                        l = l - dec.length;
                    num = coeff_array.join('') + new Array(l + 1).join(zero);
                }
            }
            return num;
         }
     }
    get_precision: {
        value: function (number) {
            var arr = Math.scientific_to_num((number + "")).split(".");
            return arr[1] ? arr[1].length : 0;
        }
    },
    sum: {
        value: function () {
            var prec = 0, sum = 0;
            for (var i = 0; i < arguments.length; i++) {
                prec = this.max(prec, this.get_precision(arguments[i]));
                sum += +arguments[i]; // force float to convert strings to number
            }
            return Math.precision(sum, prec);
        }
    }
});

ความคิดคือการใช้คณิตศาสตร์แทนผู้ประกอบการเพื่อหลีกเลี่ยงข้อผิดพลาดลอย

Math.sum จะตรวจจับความแม่นยำที่จะใช้โดยอัตโนมัติ

Math.sum รับข้อโต้แย้งจำนวนเท่าใดก็ได้


1
ฉันไม่แน่ใจว่าคุณตอบคำถามว่า " ทำไมความไม่ถูกต้องเหล่านี้ถึงเกิดขึ้น " ถึงแม้ว่า
Wai Ha Lee

ในแบบที่คุณถูก แต่ฉันมาที่นี่จากพฤติกรรมแปลก ๆ จาวาสคริปต์เกี่ยวกับปัญหานี้ ... ฉันแค่ต้องการแบ่งปันวิธีการแก้ปัญหา
bortunac

คุณยังไม่ได้ตอบคำถาม
Wai Ha Lee

k คุณมีปัญหากับสิ่งนี้ ... บอกฉันว่าจะย้ายที่ไหนหรือถ้าคุณยืนยันว่าฉันสามารถลบมันได้
bortunac

0

ฉันเพิ่งเห็นปัญหาที่น่าสนใจรอบ ๆ จุดลอย:

พิจารณาผลลัพธ์ต่อไปนี้:

error = (2**53+1) - int(float(2**53+1))
>>> (2**53+1) - int(float(2**53+1))
1

เราสามารถเห็นได้ชัดเจนเบรกพอยต์เมื่อ2**53+1- 2**53ทั้งหมดทำงานดีจน

>>> (2**53) - int(float(2**53))
0

ป้อนคำอธิบายภาพที่นี่

สิ่งนี้เกิดขึ้นเนื่องจากไบนารีที่มีความแม่นยำสองเท่า: รูปแบบทศนิยมที่มีความแม่นยำสองเท่าของ IEEE 754: binary64

จากหน้า Wikipedia สำหรับรูปแบบทศนิยมที่มีความแม่นยำสองเท่า :

เลขทศนิยมคู่ที่มีความแม่นยำสองเท่าเป็นรูปแบบที่ใช้กันทั่วไปในพีซีเนื่องจากมีช่วงกว้างกว่าจุดลอยตัวที่มีความแม่นยำเดียวทั้งๆที่มีประสิทธิภาพและต้นทุนแบนด์วิดท์ เช่นเดียวกับรูปแบบทศนิยมที่มีความแม่นยำเดียวมันไม่มีความแม่นยำกับตัวเลขจำนวนเต็มเมื่อเปรียบเทียบกับรูปแบบจำนวนเต็มขนาดเดียวกัน เป็นที่รู้จักกันทั่วไปว่าเป็นสองเท่า มาตรฐาน IEEE 754 ระบุ binary64 ว่ามี:

  • เครื่องหมายบิต: 1 บิต
  • เลขชี้กำลัง: 11 บิต
  • ความแม่นยำที่สำคัญ: 53 บิต (52 เก็บไว้อย่างชัดเจน)

ป้อนคำอธิบายภาพที่นี่

มูลค่าจริงที่สมมติโดย datum ที่มีความแม่นยำสองเท่า 64 บิตที่กำหนดพร้อมเลขชี้กำลังแบบเอนเอียงที่กำหนดและเศษส่วน 52 บิตเป็น

ป้อนคำอธิบายภาพที่นี่

หรือ

ป้อนคำอธิบายภาพที่นี่

ขอบคุณ @a_guest ที่ชี้ให้ฉันเห็น


-1

คำถามอื่นถูกตั้งชื่อซ้ำกับคำถามนี้:

ใน C ++ ทำไมเป็นผลมาจากcout << xความแตกต่างจากค่าที่ดีบักแสดงผลสำหรับx?

xในคำถามเป็นfloatตัวแปร

ตัวอย่างหนึ่งก็คือ

float x = 9.9F;

ดีบักแสดงให้เห็นว่า9.89999962การส่งออกของการดำเนินงานคือcout9.9

คำตอบก็คือcoutความแม่นยำเริ่มต้นของfloatคือ 6 ดังนั้นมันจะปัดเศษเป็นทศนิยมทศนิยม 6 หลัก

ดูที่นี่สำหรับการอ้างอิง


1
IMO - การโพสต์สิ่งนี้ที่นี่เป็นวิธีที่ผิด ฉันรู้ว่ามันน่าหงุดหงิด แต่คนที่ต้องการคำตอบสำหรับคำถามเดิม (ตอนนี้ดูเหมือนว่าถูกลบแล้ว!) จะไม่พบที่นี่ หากคุณรู้สึกว่างานของคุณสมควรได้รับการช่วยเหลือจริง ๆ ฉันขอแนะนำให้: 1) มองหาอีกคำถามหนึ่งว่านี่เป็นคำตอบจริง 2) สร้างคำถามที่ตอบเอง
สตีเฟ่นซี
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.