เหตุใดการเปลี่ยนลำดับผลรวมจึงให้ผลลัพธ์ที่แตกต่าง


294

เหตุใดการเปลี่ยนลำดับผลรวมจึงให้ผลลัพธ์ที่แตกต่าง

23.53 + 5.88 + 17.64 = 47.05

23.53 + 17.64 + 5.88 = 47.050000000000004

ทั้งJavaและJavaScriptให้ผลลัพธ์เหมือนกัน

ฉันเข้าใจว่าเนื่องจากวิธีการแสดงจำนวนจุดลอยตัวเป็นเลขฐานสองจำนวนตรรกยะบางส่วน ( เช่น 1/3 - 0.333333 ... ) จึงไม่สามารถแสดงได้อย่างแม่นยำ

ทำไมการเปลี่ยนลำดับขององค์ประกอบจึงส่งผลต่อผลลัพธ์


28
ผลรวมของจำนวนจริงนั้นเชื่อมโยงและสลับกัน คะแนนลอยตัวไม่ใช่ตัวเลขจริง ในความเป็นจริงคุณเพิ่งพิสูจน์ว่าการดำเนินการของพวกเขาไม่ได้สลับ มันค่อนข้างง่ายที่จะแสดงว่าพวกเขาไม่ได้เชื่อมโยงด้วย (เช่น(2.0^53 + 1) - 1 == 2.0^53 - 1 != 2^53 == 2^53 + (1 - 1)) ดังนั้นใช่: ระวังเมื่อเลือกลำดับของผลรวมและการดำเนินการอื่น ๆ บางภาษามีการติดตั้งภายในเพื่อแสดงผลรวมของ "ความแม่นยำสูง" (เช่น python math.fsum) ดังนั้นคุณอาจลองใช้ฟังก์ชั่นเหล่านี้แทนอัลกอริธึมแบบไร้เดียงสา
Bakuriu

1
@RBerteig ที่สามารถพิจารณาได้จากการตรวจสอบลำดับการดำเนินการของภาษาสำหรับนิพจน์ทางคณิตศาสตร์และหากไม่แสดงหมายเลขทศนิยมในหน่วยความจำจะแตกต่างกันผลลัพธ์จะเหมือนกันหากกฎของตัวดำเนินการมาก่อนเหมือนกัน อีกประเด็นที่ควรทราบ: ฉันสงสัยว่า devs ที่ใช้เวลานานในการพัฒนาแอปพลิเคชันของธนาคารจะต้องคิดออกมานานเท่าไหร่? 0000000000004เซนต์พิเศษนั้นเพิ่มขึ้นจริง ๆ !
Chris Cirefice

3
@ChrisCirefice: ถ้าคุณมี 0.00000004 เซนต์คุณทำผิด คุณไม่ควรใช้ชนิดเลขทศนิยมในการคำนวณทางการเงิน
Daniel Pryden

2
@DanielPryden อ่ามันเป็นเรื่องตลก ... เพียงแค่โยนความคิดที่ว่าคนที่ต้องได้รับการแก้ไขปัญหาประเภทนี้มีงานที่สำคัญที่สุดที่คุณรู้ถือสถานะการเงินของผู้คนและทุกสิ่งที่ . ฉันถูกเหน็บแนมมาก ...
คริส Cirefice

คำตอบ:


276

บางทีคำถามนี้อาจโง่ แต่ทำไมการเปลี่ยนลำดับขององค์ประกอบจึงส่งผลต่อผลลัพธ์

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

1/3 + 2/3 + 2/3 = (0.3333 + 0.6667) + 0.6667
                = 1.000 + 0.6667 (no rounding needed!)
                = 1.667 (where 1.6667 is rounded to 1.667)

2/3 + 2/3 + 1/3 = (0.6667 + 0.6667) + 0.3333
                = 1.333 + 0.3333 (where 1.3334 is rounded to 1.333)
                = 1.666 (where 1.6663 is rounded to 1.666)

เราไม่จำเป็นต้องมีแม้แต่จำนวนเต็มเพื่อเป็นปัญหา:

10000 + 1 - 10000 = (10000 + 1) - 10000
                  = 10000 - 10000 (where 10001 is rounded to 10000)
                  = 0

10000 - 10000 + 1 = (10000 - 10000) + 1
                  = 0 + 1
                  = 1

นี้แสดงให้เห็นอย่างชัดเจนมากขึ้นอาจเป็นไปได้ว่าส่วนหนึ่งที่สำคัญคือว่าเรามีจำนวน จำกัดเลขนัยสำคัญ - ไม่ จำกัด จำนวนตำแหน่งทศนิยม หากเราสามารถรักษาตำแหน่งทศนิยมจำนวนเท่าเดิมได้ด้วยการบวกและลบอย่างน้อยที่สุดเราก็น่าจะใช้ได้ (ตราบใดที่ค่าไม่ล้น) ปัญหาคือเมื่อคุณไปถึงตัวเลขที่ใหญ่กว่าข้อมูลที่น้อยลงจะหายไป - 1,0001 จะถูกปัดเศษเป็น 10,000 ในกรณีนี้ (นี่คือตัวอย่างของปัญหาที่Eric Lippert บันทึกไว้ในคำตอบของเขา )

เป็นสิ่งสำคัญที่จะต้องทราบว่าค่าในบรรทัดแรกของด้านขวามือจะเหมือนกันในทุกกรณีดังนั้นแม้ว่าสิ่งสำคัญคือต้องเข้าใจว่าตัวเลขทศนิยมของคุณ (23.53, 5.88, 17.64) จะไม่ถูกแสดงว่าเป็นdoubleค่าอย่างแน่นอนเพียงปัญหาเนื่องจากปัญหาที่แสดงด้านบน


10
May extend this later - out of time right now!รออย่างกระตือรือร้นเพื่อมัน @ Jon
Prateek

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

2
@ZongZhengLi: ในขณะที่มันเป็นสิ่งสำคัญที่จะเข้าใจว่ามันไม่ใช่สาเหตุที่แท้จริงในกรณีนี้ คุณสามารถเขียนเป็นตัวอย่างที่คล้ายกันที่มีค่าซึ่งจะแสดงว่าในไบนารีและดูผลเช่นเดียวกัน ปัญหาที่นี่คือการรักษาข้อมูลขนาดใหญ่และข้อมูลขนาดเล็กในเวลาเดียวกัน
Jon Skeet

1
@ Buksy: ปัดเศษเป็น10,000 - เพราะเรากำลังจัดการกับประเภทข้อมูลที่สามารถเก็บได้ 4 หลักเท่านั้น (ดังนั้น x.xxx * 10 ^ n)
Jon Skeet

3
@meteors: ไม่มันไม่ทำให้เกิดการล้น - และคุณใช้ตัวเลขผิด มันคือ 10001 ที่ถูกปัดเศษเป็น 10,000 ไม่ใช่ 1001 ที่ถูกปัดเศษเป็น 1,000 เพื่อให้ชัดเจนมากขึ้น 54321 จะถูกปัดเศษเป็น 54320 - เพราะมีตัวเลขสี่หลักเท่านั้น มีความแตกต่างใหญ่ระหว่าง "ตัวเลขสี่หลักที่สำคัญ" และ "ค่าสูงสุด 9999" อย่างที่ฉันได้พูดไปก่อนหน้านี้คุณมักจะเป็นตัวแทน x.xxx * 10 ^ n โดยที่ 10,000, x.xxx จะเท่ากับ 1.000 และ n จะเท่ากับ 4 นี่ก็เหมือนกับdoubleและfloatที่มีจำนวนมากมาก มีมากกว่า 1 ชิ้น
Jon Skeet

52

นี่คือสิ่งที่เกิดขึ้นในไบนารี อย่างที่เราทราบค่าจุดลอยตัวบางค่าไม่สามารถแสดงได้อย่างแน่นอนในรูปแบบไบนารี่ ตัวเลข 3 ตัวนี้เป็นเพียงตัวอย่างของข้อเท็จจริงนั้น

ด้วยโปรแกรมนี้ฉันจะแสดงตัวเลขฐานสิบหกของแต่ละตัวเลขและผลลัพธ์ของการเติมแต่ละครั้ง

public class Main{
   public static void main(String args[]) {
      double x = 23.53;   // Inexact representation
      double y = 5.88;    // Inexact representation
      double z = 17.64;   // Inexact representation
      double s = 47.05;   // What math tells us the sum should be; still inexact

      printValueAndInHex(x);
      printValueAndInHex(y);
      printValueAndInHex(z);
      printValueAndInHex(s);

      System.out.println("--------");

      double t1 = x + y;
      printValueAndInHex(t1);
      t1 = t1 + z;
      printValueAndInHex(t1);

      System.out.println("--------");

      double t2 = x + z;
      printValueAndInHex(t2);
      t2 = t2 + y;
      printValueAndInHex(t2);
   }

   private static void printValueAndInHex(double d)
   {
      System.out.println(Long.toHexString(Double.doubleToLongBits(d)) + ": " + d);
   }
}

printValueAndInHexวิธีการเป็นเพียงผู้ช่วยที่ฐานสิบหกเครื่องพิมพ์

ผลลัพธ์มีดังนี้:

403787ae147ae148: 23.53
4017851eb851eb85: 5.88
4031a3d70a3d70a4: 17.64
4047866666666666: 47.05
--------
403d68f5c28f5c29: 29.41
4047866666666666: 47.05
--------
404495c28f5c28f6: 41.17
4047866666666667: 47.050000000000004

4 หมายเลขแรกx, y, zและs's แสดงเลขฐานสิบหก ในการแทนค่าทศนิยม IEEE บิต 2-12 แสดงเลขชี้กำลังเลขฐานสองนั่นคือมาตราส่วนของจำนวน (บิตแรกคือบิตเครื่องหมายและบิตที่เหลือสำหรับmantissa ) เลขชี้กำลังเป็นตัวแทนจริง ๆ แล้วเป็นเลขฐานสองลบด้วย 1023

เลขชี้กำลังสำหรับ 4 หมายเลขแรกถูกแยกออกมา:

    sign|exponent
403 => 0|100 0000 0011| => 1027 - 1023 = 4
401 => 0|100 0000 0001| => 1025 - 1023 = 2
403 => 0|100 0000 0011| => 1027 - 1023 = 4
404 => 0|100 0000 0100| => 1028 - 1023 = 5

ชุดเพิ่มเติม

ตัวเลขที่สอง ( y) มีขนาดเล็กกว่า เมื่อเพิ่มตัวเลขสองตัวนี้เพื่อรับx + y2 บิตสุดท้ายของตัวเลขที่สอง ( 01) จะถูกเลื่อนออกจากช่วงและไม่ต้องคำนวณลงในการคำนวณ

การเพิ่มครั้งที่สองจะเพิ่มx + yและzเพิ่มตัวเลขสองตัวที่มีขนาดเท่ากัน

ชุดที่สองของการเพิ่ม

ที่นี่x + zเกิดขึ้นก่อน มีขนาดเท่ากัน แต่ให้จำนวนที่สูงกว่าในระดับ:

404 => 0|100 0000 0100| => 1028 - 1023 = 5

การเพิ่มครั้งที่สองจะเพิ่มx + zและyและในตอนนี้3บิตจะถูกดรอปจากyเพื่อเพิ่มหมายเลข ( 101) ที่นี่จะต้องมีการปัดขึ้นเนื่องจากผลลัพธ์คือเลขทศนิยมถัดไปขึ้น: 4047866666666666สำหรับการเพิ่มชุดแรกกับ4047866666666667การเติมชุดที่สอง ข้อผิดพลาดนั้นสำคัญพอที่จะแสดงในผลรวมของการพิมพ์

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


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

@ rgettman ในฐานะโปรแกรมเมอร์ฉันชอบคำตอบของคุณดีกว่า=)+1 สำหรับผู้ช่วยเครื่องพิมพ์ hex ของคุณ ... นั่นมันเรียบร้อยจริงๆ!
ADTC

44

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

ลองพิจารณาตัวอย่าง:

x1 = (a - b) + (c - d) + (e - f) + (g - h);

VS

x2 = (a + c + e + g) - (b + d + f + h);

VS

x3 = a - b + c - d + e - f + g - h;

เห็นได้ชัดว่าในเลขคณิตที่แน่นอนพวกเขาจะเหมือนกัน มันสนุกที่จะพยายามหาค่าสำหรับ a, b, c, d, e, f, g, h ซึ่งค่าของ x1 และ x2 และ x3 จะแตกต่างกันตามปริมาณมาก ดูว่าคุณสามารถทำได้!


คุณจะกำหนดปริมาณมากได้อย่างไร เรากำลังพูดถึงลำดับที่ 1,000 หรือเปล่า 100ths? 1 ???
Cruncher

3
@Cruncher: คำนวณผลลัพธ์ทางคณิตศาสตร์ที่แน่นอนและค่า x1 และ x2 เรียกความแตกต่างทางคณิตศาสตร์ที่แน่นอนระหว่างผลลัพธ์จริงและคำนวณแล้ว e1 และ e2 ขณะนี้มีหลายวิธีที่จะคิดเกี่ยวกับขนาดข้อผิดพลาด สิ่งแรกคือ: คุณสามารถค้นหาสถานการณ์ที่ | e1 / e2 | หรือ | e2 / e1 | มีขนาดใหญ่? เช่นคุณทำให้เกิดข้อผิดพลาดของคนอื่นได้สิบครั้งหรือไม่? สิ่งที่น่าสนใจยิ่งกว่าก็คือถ้าคุณสามารถทำให้ข้อผิดพลาดของเศษส่วนที่สำคัญของขนาดของคำตอบที่ถูกต้อง
Eric Lippert

1
ฉันรู้ว่าเขากำลังพูดถึงรันไทม์ แต่ฉันสงสัยว่า: ถ้านิพจน์นั้นเป็นคอมไพล์เวลา (พูด constexpr) คอมไพเลอร์ฉลาดพอที่จะลดข้อผิดพลาดได้หรือไม่
Kevin Hsu

@kevinhsu โดยทั่วไปไม่คอมไพเลอร์ไม่ใช่สมาร์ท แน่นอนว่าผู้รวบรวมสามารถเลือกที่จะทำการดำเนินการในเลขคณิตที่แน่นอนหากเลือกเช่นนั้น แต่โดยทั่วไปจะไม่
Eric Lippert

8
@frozenkoi: ใช่ข้อผิดพลาดสามารถไม่มีที่สิ้นสุดได้อย่างง่ายดายมาก ตัวอย่างเช่นพิจารณา C #: double d = double.MaxValue; Console.WriteLine(d + d - d - d); Console.WriteLine(d - d + d - d);- ผลลัพธ์คือ Infinity จากนั้น 0
Jon Skeet

10

สิ่งนี้ครอบคลุมมากกว่าแค่ Java และ Javascript และอาจส่งผลกระทบต่อภาษาการเขียนโปรแกรมโดยใช้แบบลอยหรือแบบสองเท่า

ในหน่วยความจำจุดลอยตัวใช้รูปแบบพิเศษตามบรรทัดของ IEEE 754 (ตัวแปลงให้คำอธิบายที่ดีกว่าที่ฉันสามารถทำได้)

อย่างไรก็ตามนี่คือเครื่องมือแปลงลอยตัว

http://www.h-schmidt.net/FloatConverter/

สิ่งที่เกี่ยวกับลำดับของการดำเนินการคือ "ความละเอียด" ของการดำเนินการ

บรรทัดแรกของคุณให้ผล 29.41 จากค่าสองค่าแรกซึ่งให้เราเป็น 2 ^ 4 เป็นเลขชี้กำลัง

บรรทัดที่สองของคุณให้ผล 41.17 ซึ่งให้เราเป็น 2 ^ 5 เป็นเลขชี้กำลัง

เรากำลังสูญเสียตัวเลขที่สำคัญโดยการเพิ่มเลขชี้กำลังซึ่งน่าจะเปลี่ยนผลลัพธ์

ลองทำเครื่องหมายบิตสุดท้ายที่ด้านขวาสุดและปิดเป็น 41.17 และคุณจะเห็นว่าบางสิ่งบางอย่างที่ "ไม่สำคัญ" เท่ากับ 1/2 ^ 23 ของเลขชี้กำลังจะเพียงพอที่จะทำให้เกิดจุดลอยตัวนี้

แก้ไข: สำหรับผู้ที่จำตัวเลขสำคัญได้จะอยู่ภายใต้หมวดหมู่นั้น 10 ^ 4 + 4999 โดยมีค่านัยสำคัญของ 1 จะเท่ากับ 10 ^ 4 ในกรณีนี้ตัวเลขที่สำคัญมีขนาดเล็กกว่ามาก แต่เราสามารถเห็นผลลัพธ์ที่มี .00000000004 ติดอยู่


9

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

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

น่าเสียดายที่เป็นปัญหาที่ทราบกันดีว่าคุณต้องจัดการกับมัน


6

ฉันเชื่อว่ามันเกี่ยวข้องกับลำดับการอพยพ ในขณะที่ผลรวมจะเหมือนกันในโลกคณิตศาสตร์ในโลกไบนารีแทน A + B + C = D มันเป็น

A + B = E
E + C = D(1)

ดังนั้นจึงมีขั้นตอนรองที่ซึ่งตัวเลขจุดลอยตัวสามารถลงได้

เมื่อคุณเปลี่ยนคำสั่งซื้อ

A + C = F
F + B = D(2)

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