พิจารณารหัสต่อไปนี้:
0.1 + 0.2 == 0.3 -> false
0.1 + 0.2 -> 0.30000000000000004
ทำไมความไม่ถูกต้องเหล่านี้เกิดขึ้น
พิจารณารหัสต่อไปนี้:
0.1 + 0.2 == 0.3 -> false
0.1 + 0.2 -> 0.30000000000000004
ทำไมความไม่ถูกต้องเหล่านี้เกิดขึ้น
คำตอบ:
เลขทศนิยมเป็นเลขฐานสองแบบนี้ ในส่วนการเขียนโปรแกรมภาษามันอยู่บนพื้นฐานของมาตรฐาน 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" ในภาษาที่คุณเลือก สิ่งเหล่านี้ไม่ควรใช้เป็นค่าความอดทน
ฉันเชื่อว่าฉันควรเพิ่มมุมมองของนักออกแบบฮาร์ดแวร์เนื่องจากฉันออกแบบและสร้างฮาร์ดแวร์จุดลอยตัว การทราบที่มาของข้อผิดพลาดอาจช่วยในการทำความเข้าใจสิ่งที่เกิดขึ้นในซอฟต์แวร์และท้ายที่สุดฉันหวังว่าสิ่งนี้จะช่วยอธิบายสาเหตุที่ทำให้เกิดข้อผิดพลาดจุดลอยตัวและดูเหมือนจะสะสมอยู่ตลอดเวลา
จากมุมมองทางวิศวกรรมการดำเนินการจุดลอยตัวส่วนใหญ่จะมีองค์ประกอบของข้อผิดพลาดเนื่องจากฮาร์ดแวร์ที่ใช้การคำนวณจุดลอยตัวจำเป็นต้องมีข้อผิดพลาดน้อยกว่าครึ่งหนึ่งของหนึ่งหน่วยในสถานที่สุดท้าย ดังนั้นฮาร์ดแวร์จำนวนมากจะหยุดที่ความแม่นยำที่จำเป็นเท่านั้นที่จะให้ข้อผิดพลาดน้อยกว่าครึ่งหนึ่งของหน่วยในสถานที่สุดท้ายสำหรับการดำเนินการเดียวซึ่งเป็นปัญหาโดยเฉพาะอย่างยิ่งในการแบ่งจุดลอยตัว สิ่งที่ถือว่าเป็นการดำเนินการครั้งเดียวขึ้นอยู่กับจำนวนตัวถูกดำเนินการหน่วยที่ใช้ ส่วนใหญ่เป็นสอง แต่บางหน่วยใช้ตัวถูกดำเนินการ 3 ตัวขึ้นไป ด้วยเหตุนี้จึงไม่มีการรับประกันว่าการทำงานซ้ำ ๆ จะส่งผลให้เกิดข้อผิดพลาดที่พึงประสงค์เนื่องจากข้อผิดพลาดจะเพิ่มขึ้นเมื่อเวลาผ่านไป
โปรเซสเซอร์ส่วนใหญ่ตามมาตรฐาน IEEE-754แต่บางตัวใช้ denormalized หรือมาตรฐานที่แตกต่างกัน ตัวอย่างเช่นมีโหมด denormalized ใน IEEE-754 ซึ่งอนุญาตให้แสดงหมายเลขจุดลอยตัวที่น้อยมากโดยใช้ความแม่นยำ อย่างไรก็ตามต่อไปนี้จะครอบคลุมโหมดการทำให้เป็นมาตรฐานของ IEEE-754 ซึ่งเป็นโหมดการทำงานทั่วไป
ในมาตรฐาน IEEE-754 นักออกแบบฮาร์ดแวร์ได้รับอนุญาตให้มีข้อผิดพลาด / epsilon ใด ๆ ตราบใดที่มันน้อยกว่าครึ่งหนึ่งของหนึ่งหน่วยในสถานที่สุดท้ายและผลลัพธ์จะต้องน้อยกว่าครึ่งหนึ่งของหนึ่งหน่วยในที่สุด สถานที่สำหรับการดำเนินการอย่างใดอย่างหนึ่ง สิ่งนี้อธิบายว่าทำไมเมื่อมีการดำเนินการซ้ำหลายครั้งข้อผิดพลาดจะเพิ่มขึ้น สำหรับความแม่นยำสองเท่าของ IEEE-754 นี่คือบิตที่ 54 เนื่องจาก 53 บิตถูกใช้เพื่อแทนส่วนที่เป็นตัวเลข (ปกติ) หรือที่เรียกว่า mantissa ของจำนวนจุดลอยตัว (เช่น 5.3 ใน 5.3e5) ส่วนต่อไปจะกล่าวถึงรายละเอียดเพิ่มเติมเกี่ยวกับสาเหตุของข้อผิดพลาดของฮาร์ดแวร์ในการดำเนินการจุดลอยตัวต่างๆ
สาเหตุหลักของข้อผิดพลาดในการหารทศนิยมเป็นอัลกอริทึมการหารที่ใช้ในการคำนวณความฉลาด ระบบคอมพิวเตอร์ส่วนใหญ่คำนวณการหารด้วยการคูณด้วยอินเวอร์Z=X/Y
สZ = 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; แต่ละรายการมีการแก้ไขตามอัลกอริทึมการหารในความพยายามที่จะทำให้เกิดข้อผิดพลาดต่ำที่สุด อย่างไรก็ตามในกรณีใด ๆ การแลกเปลี่ยนทั้งหมดเป็นการประมาณของการแลกเปลี่ยนซึ่งกันและกันและแนะนำองค์ประกอบของข้อผิดพลาด ทั้งการหารช้าและวิธีการหารอย่างรวดเร็วคำนวณความฉลาดทางซ้ำนั่นคือจำนวนบิตของความฉลาดทางคำนวณในแต่ละขั้นตอนจากนั้นผลลัพธ์จะถูกลบออกจากเงินปันผลและตัวหารจะทำซ้ำขั้นตอนจนกว่าข้อผิดพลาดจะน้อยกว่าครึ่งหนึ่ง หน่วยในสถานที่สุดท้าย วิธีการหารแบบช้าจะคำนวณจำนวนหลักของความฉลาดทางในแต่ละขั้นตอนและมักจะไม่แพงในการสร้างและวิธีการหารอย่างรวดเร็วจะคำนวณจำนวนตัวเลขของตัวเลขต่อขั้นและมักจะมีราคาแพงกว่าในการสร้าง ส่วนที่สำคัญที่สุดของวิธีการแบ่งคือส่วนใหญ่ของพวกเขาพึ่งพาการคูณซ้ำโดยการประมาณซึ่งกันและกันดังนั้นพวกเขาจึงมีแนวโน้มที่จะเกิดข้อผิดพลาด
สาเหตุของข้อผิดพลาดในการปัดเศษในการดำเนินการทั้งหมดคือโหมดที่แตกต่างกันของการตัดปลายของคำตอบสุดท้ายที่ IEEE-754 อนุญาต มีการตัดปลายปัดเศษเป็นศูนย์ปัดเศษใกล้สุด (ค่าเริ่มต้น)ปัดเศษขึ้นและปัดเศษขึ้น วิธีการทั้งหมดแนะนำองค์ประกอบของข้อผิดพลาดน้อยกว่าหนึ่งหน่วยในสถานที่สุดท้ายสำหรับการดำเนินการเดียว เมื่อเวลาผ่านไปและการดำเนินการซ้ำแล้วซ้ำอีกการตัดปลายยังเพิ่มข้อผิดพลาดผลลัพธ์ การตัดทอนข้อผิดพลาดนี้เป็นปัญหาอย่างยิ่งในการยกกำลังซึ่งเกี่ยวข้องกับการคูณซ้ำหลายรูปแบบ
เนื่องจากฮาร์ดแวร์ที่ใช้การคำนวณจุดลอยตัวจำเป็นต้องให้ผลลัพธ์ที่มีข้อผิดพลาดน้อยกว่าครึ่งหนึ่งของหนึ่งหน่วยในตำแหน่งสุดท้ายสำหรับการดำเนินการครั้งเดียวข้อผิดพลาดจะเพิ่มขึ้นจากการดำเนินการซ้ำ ๆ นี่คือเหตุผลที่ในการคำนวณที่ต้องการข้อผิดพลาดขอบเขตนักคณิตศาสตร์ใช้วิธีการเช่นการใช้เลขคู่ที่ใกล้เคียงที่สุดในสถานที่สุดท้ายของ IEEE-754 เพราะเมื่อเวลาผ่านไปข้อผิดพลาดมีแนวโน้มที่จะยกเลิกกัน ออกและช่วงเวลาทางคณิตศาสตร์รวมกับรูปแบบของการปัดเศษ IEEE 754เพื่อทำนายข้อผิดพลาดในการปัดเศษและแก้ไขให้ถูกต้อง เนื่องจากข้อผิดพลาดสัมพัทธ์ต่ำเมื่อเทียบกับโหมดการปัดเศษอื่น ๆ การปัดเศษเป็นเลขคู่ที่ใกล้ที่สุด (ในตำแหน่งสุดท้าย) คือโหมดการปัดเศษเริ่มต้นของ IEEE-754
โปรดทราบว่าโหมดการปัดเศษเริ่มต้นซึ่งเป็นเลขคู่ที่ใกล้ที่สุดถึงจุดสุดท้ายรับประกันความผิดพลาดน้อยกว่าครึ่งหนึ่งของหนึ่งหน่วยในตำแหน่งสุดท้ายสำหรับการดำเนินการครั้งเดียว การใช้การปัดเศษปัดเศษขึ้นและปัดเศษตามลำพังอาจส่งผลให้เกิดข้อผิดพลาดที่มากกว่าครึ่งหนึ่งของหนึ่งหน่วยในสถานที่สุดท้าย แต่น้อยกว่าหนึ่งหน่วยในสถานที่สุดท้ายดังนั้นจึงไม่แนะนำให้ใช้โหมดเหล่านี้ ใช้ในการคำนวณช่วงเวลา
ในระยะสั้นเหตุผลพื้นฐานสำหรับข้อผิดพลาดในการดำเนินการจุดลอยตัวคือการรวมกันของการตัดทอนในฮาร์ดแวร์และการตัดทอนของซึ่งกันและกันในกรณีของการแบ่ง เนื่องจากมาตรฐาน IEEE-754 ต้องการเพียงข้อผิดพลาดน้อยกว่าครึ่งหนึ่งของหน่วยในสถานที่สุดท้ายสำหรับการดำเนินการเดียวข้อผิดพลาดจุดลอยตัวมากกว่าการดำเนินการซ้ำจะเพิ่มขึ้นเว้นแต่จะได้รับการแก้ไข
เมื่อคุณแปลง. 1 หรือ 1/10 เป็นฐาน 2 (เลขฐานสอง) คุณจะได้รูปแบบการทำซ้ำหลังจุดทศนิยมเช่นเดียวกับการพยายามแทนค่า 1/3 ในฐาน 10 ค่าไม่ถูกต้องและคุณไม่สามารถทำได้ คณิตศาสตร์ที่แน่นอนกับมันโดยใช้วิธีการจุดลอยปกติ
คำตอบส่วนใหญ่ที่นี่ตอบคำถามนี้ในเงื่อนไขทางเทคนิคที่แห้งมาก ฉันต้องการพูดถึงเรื่องนี้ในแง่ที่ว่ามนุษย์ทั่วไปสามารถเข้าใจได้
ลองนึกภาพว่าคุณกำลังพยายามหั่นพิซซ่า คุณมีเครื่องตัดพิซซ่าหุ่นยนต์ที่สามารถตัดพิซซ่าได้ครึ่งส่วน มันอาจลดลงครึ่งหนึ่งของพิซซ่าทั้งหมดหรือลดลงครึ่งหนึ่งของชิ้นที่มีอยู่ แต่ไม่ว่าในกรณีใดการแบ่งครึ่งจะแน่นอนเสมอ
ที่ตัดพิซซ่ามีการเคลื่อนไหวที่ดีมากและหากคุณเริ่มต้นด้วยพิซซ่าทั้งหมดแล้วลดลงครึ่งหนึ่งนั้นและยังคงลดลงครึ่งหนึ่งชิ้นที่เล็กที่สุดในแต่ละครั้งที่คุณสามารถทำได้ลดลงครึ่งหนึ่ง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 บางภาษาการเขียนโปรแกรมยังให้ตัดพิซซ่าที่สามารถแยกชิ้นเข้าไปในสิบที่แน่นอน แม้ว่าตัวตัดพิซซ่าดังกล่าวนั้นผิดปกติ แต่ถ้าคุณมีสิทธิ์เข้าถึงได้คุณควรใช้เมื่อมีความสำคัญที่จะต้องได้รับหนึ่งในสิบหรือหนึ่งในห้าของชิ้น
ข้อผิดพลาดในการปัดเศษทศนิยม 0.1 ไม่สามารถแสดงได้อย่างถูกต้องในฐาน -2 ในขณะที่ฐาน 10 เนื่องจากปัจจัยหลักที่ขาดหายไปของ 5 เช่นเดียวกับ 1/3 ใช้จำนวนอนันต์ของตัวเลขเพื่อแสดงในทศนิยม แต่เป็น "0.1" ในฐาน -3 0.1 รับจำนวนอนันต์ของตัวเลขในฐาน 2 โดยที่ไม่ได้อยู่ในฐาน -10 และคอมพิวเตอร์ไม่มีหน่วยความจำไม่ จำกัด
นอกเหนือจากคำตอบที่ถูกต้องอื่น ๆ คุณอาจต้องการพิจารณาปรับขนาดค่าของคุณเพื่อหลีกเลี่ยงปัญหาเกี่ยวกับการคำนวณเลขทศนิยม
ตัวอย่างเช่น:
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)
คำตอบของฉันค่อนข้างยาวดังนั้นฉันจึงแบ่งออกเป็นสามส่วน เนื่องจากคำถามเกี่ยวกับคณิตศาสตร์จุดลอยตัวฉันได้ให้ความสำคัญกับสิ่งที่เครื่องทำจริง ฉันได้ทำให้มันมีความแม่นยำเป็นสองเท่า (64 บิต) แต่ข้อโต้แย้งนี้ใช้กับเลขคณิตจุดลอยตัวอย่างเท่าเทียมกัน
คำนำ
IEEE 754 แม่นยำสองรูปแบบไบนารีจุดลอยตัว (binary64)จำนวนหมายถึงจำนวนของรูปแบบ
ค่า = (-1) ^ s * (1.m 51ม. 50 ... ม. 2ม. 1ม. 0 ) 2 * 2 e-1023
ใน 64 บิต:
1
ถ้าตัวเลขเป็นลบ0
มิฉะนั้น1 11.
อยู่เสมอ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 ประกอบด้วยทั้งหมด1
s) คือ 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
: บิตสุดท้ายจะถูกปัดเศษ
ตัวเลขทศนิยมที่เก็บไว้ในคอมพิวเตอร์ประกอบด้วยสองส่วนคือจำนวนเต็มและเลขชี้กำลังที่ฐานถูกนำไปใช้และคูณด้วยส่วนจำนวนเต็ม
ถ้าคอมพิวเตอร์ที่กำลังทำงานอยู่ในฐาน 10 0.1
จะ1 x 10⁻¹
, 0.2
จะเป็น2 x 10⁻¹
และจะเป็น0.3
3 x 10⁻¹
คณิตศาสตร์จำนวนเต็มเป็นเรื่องง่ายและที่แน่นอนเพื่อเพิ่มอย่างเห็นได้ชัดจะส่งผลให้0.1 + 0.2
0.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
หมายเลขจุดลอยตัวจะถูกปัดเศษเพื่อแสดงผล
ข้อผิดพลาดในการปัดเศษทศนิยม จากสิ่งที่นักวิทยาศาสตร์คอมพิวเตอร์ทุกคนควรรู้เกี่ยวกับเลขคณิตทศนิยม :
การบีบจำนวนจริงจำนวนมากเป็นจำนวนบิตที่ จำกัด ต้องมีการแสดงโดยประมาณ แม้ว่าจะมีจำนวนเต็มไม่ จำกัด จำนวนมากในโปรแกรมส่วนใหญ่ผลลัพธ์ของการคำนวณจำนวนเต็มสามารถเก็บไว้ใน 32 บิต ในทางตรงกันข้ามหากมีจำนวนบิตคงที่การคำนวณส่วนใหญ่ที่มีจำนวนจริงจะสร้างปริมาณที่ไม่สามารถแสดงได้อย่างแน่นอนโดยใช้บิตจำนวนมาก ดังนั้นผลลัพธ์ของการคำนวณทศนิยมจะต้องถูกปัดเศษเพื่อให้พอดีกับการแทนค่า จำกัด ข้อผิดพลาดในการปัดเศษเป็นคุณลักษณะเฉพาะของการคำนวณทศนิยม
วิธีแก้ปัญหาของฉัน:
function add(a, b, precision) {
var x = Math.pow(10, precision || 2);
return (Math.round(a * x) + Math.round(b * x)) / x;
}
ความแม่นยำหมายถึงจำนวนหลักที่คุณต้องการรักษาหลังจุดทศนิยมระหว่างการเพิ่ม
มีคำตอบที่ดีมากมายถูกโพสต์ แต่ฉันต้องการเพิ่มอีกหนึ่งรายการ
ไม่สามารถแสดงตัวเลขทั้งหมดได้ด้วยการลอยตัว / เป็นสองเท่า ตัวอย่างเช่นหมายเลข "0.2" จะแสดงเป็น "0.200000003" ด้วยความแม่นยำเดียวในมาตรฐาน IEEE754
แบบจำลองสำหรับเก็บตัวเลขจริงภายใต้ประทุนแสดงถึงจำนวนลอยตัวเช่น
แม้ว่าคุณสามารถพิมพ์0.2
ได้อย่างง่ายดายFLT_RADIX
และDBL_RADIX
เป็น 2; ไม่ใช่ 10 สำหรับคอมพิวเตอร์ที่มี FPU ซึ่งใช้ "มาตรฐาน IEEE สำหรับเลขฐานสองทศนิยม (ISO / IEEE Std 754-1985)"
ดังนั้นมันจึงยากที่จะเป็นตัวแทนของตัวเลขเหล่านี้อย่างแน่นอน แม้ว่าคุณจะระบุตัวแปรนี้อย่างชัดเจนโดยไม่มีการคำนวณระดับกลางใด ๆ
สถิติบางอย่างเกี่ยวข้องกับคำถามความแม่นยำสองเท่าที่มีชื่อเสียงนี้
เมื่อมีการเพิ่มค่าทั้งหมด ( 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%)
สรุป
เลขคณิตของทศนิยมนั้นแน่นอน แต่น่าเสียดายที่มันไม่เข้ากันได้ดีกับการแทนเลขฐาน -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 หรืออากาศพลศาสตร์แบบนั้นได้
nextafter()
กับการเพิ่มหรือลดจำนวนเต็มในการแทนเลขฐานสองของการลอยแบบ IEEE นอกจากนี้คุณสามารถเปรียบเทียบการลอยเป็นจำนวนเต็มและได้รับคำตอบที่ถูกต้องยกเว้นเมื่อทั้งคู่เป็นค่าลบ (เนื่องจากส่วนประกอบการลงชื่อกับขนาดของ 2)
คุณลองใช้โซลูชันเทปพันท่อหรือไม่?
ลองพิจารณาว่าเมื่อเกิดข้อผิดพลาดและแก้ไขโดยย่อถ้าข้อความไม่สวย แต่สำหรับปัญหาบางอย่างมันเป็นทางออกเดียวและนี่เป็นหนึ่งในนั้น
if( (n * 0.1) < 100.0 ) { return n * 0.1 - 0.000000000000001 ;}
else { return n * 0.1 + 0.000000000000001 ;}
ฉันมีปัญหาเดียวกันในโครงการจำลองทางวิทยาศาสตร์ใน c # และฉันสามารถบอกคุณได้ว่าถ้าคุณไม่สนใจเอฟเฟกต์ของผีเสื้อมันจะกลายเป็นมังกรอ้วนตัวโตและกัดคุณใน **
เพื่อที่จะเสนอทางออกที่ดีที่สุดฉันสามารถพูดได้ว่าฉันค้นพบวิธีการดังต่อไปนี้:
parseFloat((0.1 + 0.2).toFixed(10)) => Will return 0.3
ให้ฉันอธิบายว่าทำไมมันจึงเป็นทางออกที่ดีที่สุด ตามที่คนอื่น ๆ ที่กล่าวถึงข้างต้นคำตอบมันเป็นความคิดที่ดีที่จะใช้พร้อมที่จะใช้ฟังก์ชั่น Javascript toFixed () เพื่อแก้ปัญหา แต่ส่วนใหญ่คุณจะพบกับปัญหาบางอย่าง
ลองนึกภาพคุณกำลังจะเพิ่มขึ้นสองหมายเลขลอยเหมือน0.2
และนี่คือ:0.7
0.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
จะไม่ทำงานเลยแม้ว่าจะดูเหมือนกัน! ฉันชอบวิธีแก้ปัญหาแรกเพราะฉันสามารถใช้มันเป็นฟังก์ชั่นที่แปลงอินพุตโฟลล์เป็นเอาท์พุทโฟลิโอที่แม่นยำ
ตัวเลขแปลก ๆ เหล่านั้นปรากฏขึ้นเนื่องจากคอมพิวเตอร์ใช้ระบบเลขฐานสอง (ฐาน 2) เพื่อการคำนวณในขณะที่เราใช้ทศนิยม (ฐาน 10)
มีตัวเลขเศษส่วนส่วนใหญ่ที่ไม่สามารถแสดงได้อย่างแม่นยำทั้งในไบนารีหรือทศนิยมหรือทั้งสองอย่าง ผลลัพธ์ - ผลลัพธ์ตัวเลขปัดเศษ (แต่แม่นยำ)
คำถามที่ซ้ำกันจำนวนมากของคำถามนี้ถามเกี่ยวกับผลกระทบของการปัดเศษทศนิยมตามจำนวนที่ระบุ ในทางปฏิบัติมันง่ายกว่าที่จะรับความรู้สึกว่ามันทำงานอย่างไรโดยการดูผลลัพธ์ที่แน่นอนของการคำนวณดอกเบี้ยมากกว่าเพียงแค่อ่านเกี่ยวกับมัน บางภาษามีวิธีการทำเช่น - การแปลง a float
หรือdouble
เป็นBigDecimal
Java
เนื่องจากนี่เป็นคำถามเกี่ยวกับผู้ไม่เชื่อเรื่องพระเจ้าจึงจำเป็นต้องมีเครื่องมือที่ไม่เชื่อเรื่องภาษาเช่น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 ซึ่งลงท้ายด้วยเลขคู่ดังนั้นจึงเป็นผลลัพธ์ที่ถูกต้อง
เนื่องจากไม่มีใครพูดถึงเรื่องนี้ ...
ภาษาระดับสูงบางภาษาเช่น 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)
คู่และพวกเขาอาจให้ผลลัพธ์ที่แม่นยำกว่าเลขทศนิยมทศนิยม
วิธีแก้ปัญหาเหล่านี้ไม่สมบูรณ์แบบ (โดยเฉพาะถ้าเราดูการแสดงหรือถ้าเราต้องการความแม่นยำสูงมาก) แต่ก็ยังแก้ปัญหาจำนวนมากด้วยเลขฐานสองทศนิยม
ฉันสามารถเพิ่ม; คนมักจะคิดว่านี่เป็นปัญหาคอมพิวเตอร์ แต่ถ้าคุณนับด้วยมือของคุณ (ฐาน 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 นิ้วในแต่ละมือจะถามว่าทำไมคณิตศาสตร์ทศนิยมของคุณถึงแตก ...
ชนิดของเลขทศนิยมที่สามารถนำมาใช้ในคอมพิวเตอร์ดิจิทัลจำเป็นต้องใช้การประมาณจำนวนจริงและการดำเนินการกับพวกเขา ( รุ่นมาตรฐานทำงานได้มากกว่าห้าสิบหน้าของเอกสารและมีคณะกรรมการเพื่อจัดการกับ errata และการปรับแต่งเพิ่มเติม)
การประมาณนี้เป็นส่วนผสมของการประมาณค่าของชนิดต่าง ๆ ซึ่งแต่ละอย่างสามารถละเว้นหรือคิดอย่างรอบคอบเนื่องจากลักษณะเฉพาะของการเบี่ยงเบนจากความแน่นอน นอกจากนี้ยังเกี่ยวข้องกับกรณีพิเศษที่ชัดเจนทั้งในระดับฮาร์ดแวร์และซอฟต์แวร์ที่คนส่วนใหญ่เดินผ่านมาในขณะที่แสร้งทำเป็นไม่สังเกตเห็น
หากคุณต้องการความแม่นยำที่ไม่สิ้นสุด (โดยใช้หมายเลข instead ตัวอย่างเช่นแทนที่จะเป็นหนึ่งในสแตนเลสที่สั้นกว่า) คุณควรเขียนหรือใช้โปรแกรมคณิตศาสตร์สัญลักษณ์แทน
แต่ถ้าคุณไม่เป็นไรกับความคิดที่ว่าบางครั้งคณิตศาสตร์แบบลอยตัวนั้นคลุมเครือในค่าและตรรกะและข้อผิดพลาดสามารถสะสมได้อย่างรวดเร็วและคุณสามารถเขียนข้อกำหนดและการทดสอบของคุณเพื่ออนุญาตสิ่งนั้นได้ FPU ของคุณ
เพื่อความสนุกฉันเล่นกับการแสดงลอยตามคำจำกัดความจาก 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 นั้นถูกรบกวนด้วยเช่นกัน อย่าลืมที่จะกำหนดปัจจัยในการscale
bc
ผลรวมที่ปรากฏคือสิ่งที่อยู่ภายในฮาร์ดแวร์ ค่าที่คุณได้รับจากการคำนวณมันขึ้นอยู่กับขนาดที่คุณตั้งไว้ ฉันตั้งค่าscale
ปัจจัยเป็น 15 ในทางคณิตศาสตร์ด้วยความแม่นยำที่ไม่สิ้นสุดดูเหมือนว่าจะเป็น 1,000,000,000
อีกวิธีในการดู: ใช้เป็น 64 บิตเพื่อแสดงตัวเลข ดังนั้นจึงไม่มีทางที่มากกว่า 2 ** 64 = 18,446,744,073,709,551,616 ตัวเลขที่แตกต่างกันสามารถแสดงได้อย่างแม่นยำ
อย่างไรก็ตาม Math กล่าวว่ามีทศนิยมจำนวนมากระหว่าง 0 และ 1 IEE 754 กำหนดการเข้ารหัสเพื่อใช้ 64 บิตเหล่านี้อย่างมีประสิทธิภาพสำหรับพื้นที่หมายเลขที่มีขนาดใหญ่กว่าบวก NaN และ +/- Infinity ดังนั้นจึงมีช่องว่างระหว่างตัวเลขที่ถูกต้อง ตัวเลขโดยประมาณเท่านั้น
น่าเสียดายที่ 0.3 อยู่ในช่องว่าง
ลองนึกภาพการทำงานในฐานสิบด้วยพูดความแม่นยำ 8 หลัก คุณตรวจสอบว่า
1/3 + 2 / 3 == 1
และเรียนรู้ว่าสิ่งนี้จะกลับfalse
มา ทำไม? เช่นเดียวกับตัวเลขจริงที่เรามี
1/3 = 0.333 ....และ2/3 = 0.666 ....
ตัดทอนที่ตำแหน่งทศนิยมแปดตำแหน่งเราได้
0.33333333 + 0.66666666 = 0.99999999
ซึ่งเป็นของหลักสูตรที่แตกต่างจากโดยตรง1.00000000
0.00000001
สถานการณ์สำหรับเลขฐานสองที่มีจำนวนบิตคงที่นั้นคล้ายคลึงกันทุกประการ ในฐานะที่เป็นตัวเลขจริงเรามี
1/10 = 0.0001100110011001100 ... (ฐาน 2)
และ
1/5 = 0.0011001100110011001 ... (ฐาน 2)
ถ้าเราตัดส่วนเหล่านี้ให้เป็นเจ็ดบิตเราก็จะได้
0.0001100 + 0.0011001 = 0.0100101
ในขณะที่ในอีกทางหนึ่ง
3/10 = 0.01001100110011 ... (ฐาน 2)
ซึ่งตัดออกไปบิตเจ็ดเป็นและสิ่งเหล่านี้แตกต่างกันโดยตรง0.0100110
0.0000001
สถานการณ์ที่แน่นอนนั้นละเอียดกว่าเล็กน้อยเนื่องจากตัวเลขเหล่านี้มักถูกเก็บไว้ในรูปแบบทางวิทยาศาสตร์ ตัวอย่างเช่นแทนที่จะเก็บ 1/10 เพราะ0.0001100
เราอาจเก็บไว้เป็นอย่างอื่น1.10011 * 2^-4
ขึ้นอยู่กับจำนวนบิตที่เราจัดสรรสำหรับเลขชี้กำลังและแมนทิสซา สิ่งนี้มีผลต่อจำนวนความแม่นยำที่คุณได้รับสำหรับการคำนวณของคุณ
ผลที่สุดคือเนื่องจากข้อผิดพลาดในการปัดเศษคุณไม่ต้องการใช้ == สำหรับตัวเลขทศนิยม คุณสามารถตรวจสอบว่าค่าสัมบูรณ์ของความแตกต่างนั้นน้อยกว่าจำนวนคงที่บางส่วนแทนหรือไม่
ตั้งแต่ Python 3.5คุณสามารถใช้math.isclose()
ฟังก์ชั่นสำหรับทดสอบความเท่าเทียมกันโดยประมาณ:
>>> import math
>>> math.isclose(0.1 + 0.2, 0.3)
True
>>> 0.1 + 0.2 == 0.3
False
เนื่องจากกระทู้นี้แยกออกไปเล็กน้อยในการอภิปรายทั่วไปเกี่ยวกับการใช้งานจุดลอยตัวปัจจุบันฉันต้องการเพิ่มว่ามีโครงการในการแก้ไขปัญหาของพวกเขา
ลองดูที่https://posithub.org/ตัวอย่างเช่นซึ่งแสดงประเภทตัวเลขที่เรียกว่า posit (และ unum บรรพบุรุษของมัน) ที่สัญญาว่าจะให้ความแม่นยำที่ดีขึ้นด้วยบิตที่น้อยลง หากความเข้าใจของฉันถูกต้องก็จะแก้ไขปัญหาในคำถาม ค่อนข้างโครงการที่น่าสนใจคนที่อยู่เบื้องหลังมันเป็นนักคณิตศาสตร์มันดร. จอห์นกุสตาฟ สิ่งทั้งหมดเป็นโอเพนซอร์ซโดยมีการใช้งานจริงใน C / C ++, Python, Julia และ C # ( https://hastlayer.com/arithmetics )
จริงๆแล้วมันค่อนข้างง่าย เมื่อคุณมีระบบฐาน 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 คอมพิวเตอร์ทำงานในเมื่อคุณทำคณิตศาสตร์ในทศนิยมซ้ำเหล่านี้
ตัวเลขทศนิยมเช่น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 )
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 รับข้อโต้แย้งจำนวนเท่าใดก็ได้
พิจารณาผลลัพธ์ต่อไปนี้:
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 ที่ชี้ให้ฉันเห็น
คำถามอื่นถูกตั้งชื่อซ้ำกับคำถามนี้:
ใน C ++ ทำไมเป็นผลมาจากcout << x
ความแตกต่างจากค่าที่ดีบักแสดงผลสำหรับx
?
x
ในคำถามเป็นfloat
ตัวแปร
ตัวอย่างหนึ่งก็คือ
float x = 9.9F;
ดีบักแสดงให้เห็นว่า9.89999962
การส่งออกของการดำเนินงานคือcout
9.9
คำตอบก็คือcout
ความแม่นยำเริ่มต้นของfloat
คือ 6 ดังนั้นมันจะปัดเศษเป็นทศนิยมทศนิยม 6 หลัก
ดูที่นี่สำหรับการอ้างอิง