ทำไม Math.round (0.49999999999999994) ส่งคืน 1


567

ในโปรแกรมต่อไปนี้คุณจะเห็นว่าแต่ละค่าน้อยกว่า.5จะถูกปัดเศษลงยกเว้น0.5จะถูกปัดเศษลงยกเว้น

for (int i = 10; i >= 0; i--) {
    long l = Double.doubleToLongBits(i + 0.5);
    double x;
    do {
        x = Double.longBitsToDouble(l);
        System.out.println(x + " rounded is " + Math.round(x));
        l--;
    } while (Math.round(x) > i);
}

พิมพ์

10.5 rounded is 11
10.499999999999998 rounded is 10
9.5 rounded is 10
9.499999999999998 rounded is 9
8.5 rounded is 9
8.499999999999998 rounded is 8
7.5 rounded is 8
7.499999999999999 rounded is 7
6.5 rounded is 7
6.499999999999999 rounded is 6
5.5 rounded is 6
5.499999999999999 rounded is 5
4.5 rounded is 5
4.499999999999999 rounded is 4
3.5 rounded is 4
3.4999999999999996 rounded is 3
2.5 rounded is 3
2.4999999999999996 rounded is 2
1.5 rounded is 2
1.4999999999999998 rounded is 1
0.5 rounded is 1
0.49999999999999994 rounded is 1
0.4999999999999999 rounded is 0

ฉันใช้การอัพเดท Java 6 31


1
บน java 1.7.0 ใช้ได้ ok i.imgur.com/hZeqx.png
Coffee

2
@Adel: ดูความคิดเห็นของฉันเกี่ยวกับคำตอบของ Oliดูเหมือนว่า Java 6 จะดำเนินการนี้ (และเอกสารที่ทำ ) ในลักษณะที่อาจทำให้สูญเสียความแม่นยำเพิ่มเติมโดยการเพิ่ม0.5หมายเลขและจากนั้นใช้floor; Java 7 ไม่มีเอกสารเป็นอย่างนั้นอีกต่อไป (น่าจะเป็น / หวังว่าเพราะพวกเขาแก้ไขมัน)
TJ Crowder

1
มันเป็นข้อผิดพลาดในโปรแกรมทดสอบที่ฉันเขียน ;)
Peter Lawrey

1
อีกตัวอย่างหนึ่งที่แสดงค่าจุดลอยตัวไม่สามารถนำไปใช้ที่ค่าใบหน้า
Michaël Roy

1
หลังจากคิดเรื่องนี้แล้ว ฉันไม่เห็นปัญหา 0.49999999999999994 นั้นใหญ่กว่าจำนวนที่น้อยที่สุดที่สามารถแทนได้น้อยกว่า 0.5 และการแทนในรูปแบบทศนิยมที่มนุษย์สามารถอ่านได้นั้นเป็นตัวประมาณที่พยายามหลอกเรา
Michaël Roy

คำตอบ:


574

สรุป

ใน Java 6 (และสันนิษฐานว่าก่อนหน้านี้) จะดำเนินการเป็นround(x) 1 นี่คือข้อผิดพลาดสเปคสำหรับกรณีทางพยาธิวิทยานี้ 2 Java 7 ไม่มีการบังคับใช้การใช้งานที่ไม่สมบูรณ์นี้อีกต่อไป 3floor(x+0.5)

ปัญหา

0.5 + 0.49999999999999994 คือ 1 ในความแม่นยำสองเท่า:

static void print(double d) {
    System.out.printf("%016x\n", Double.doubleToLongBits(d));
}

public static void main(String args[]) {
    double a = 0.5;
    double b = 0.49999999999999994;

    print(a);      // 3fe0000000000000
    print(b);      // 3fdfffffffffffff
    print(a+b);    // 3ff0000000000000
    print(1.0);    // 3ff0000000000000
}

นี่คือเนื่องจาก 0.49999999999999994 มีเลขชี้กำลังขนาดเล็กกว่า 0.5 ดังนั้นเมื่อมีการเพิ่มจะมีการเปลี่ยน mantissa และ ULP จะใหญ่ขึ้น

การแก้ไขปัญหา

ตั้งแต่ Java 7, OpenJDK (ตัวอย่าง) ใช้มันดังนี้: 4

public static long round(double a) {
    if (a != 0x1.fffffffffffffp-2) // greatest double value less than 0.5
        return (long)floor(a + 0.5d);
    else
        return 0;
}

1. http://docs.oracle.com/javase/6/docs/api/java/lang/Math.html#round%28double%29

2. http://bugs.java.com/bugdatabase/view_bug.do?bug_id=6430675 (เครดิตถึง @SimonNickerson สำหรับการค้นหาสิ่งนี้)

3. http://docs.oracle.com/javase/7/docs/api/java/lang/Math.html#round%28double%29

4. http://grepcode.com/file/repository.grepcode.com/java/root/jdk/openjdk/7u40-b43/java/lang/Math.java#Math.round%28double%29


ฉันไม่เห็นคำจำกัดความของroundในJavadoc สำหรับMath.roundหรือในภาพรวมของMathชั้นเรียน
TJ Crowder

3
@ Oli: โอ้ตอนนี้น่าสนใจพวกเขาเอาบิตนั้นออกมาสำหรับ Java 7 (เอกสารที่ฉันเชื่อมโยง) - บางทีเพื่อหลีกเลี่ยงการทำให้เกิดพฤติกรรมแปลก ๆ แบบนี้โดยการกระตุ้นการสูญเสียความแม่นยำ (เพิ่มเติม)
TJ Crowder

@TJCrowder: ใช่มันน่าสนใจ คุณรู้หรือไม่ว่ามีเอกสาร "บันทึกย่อประจำรุ่น" / "การปรับปรุง" ใด ๆ สำหรับ Java แต่ละเวอร์ชันเพื่อให้เราสามารถตรวจสอบสมมติฐานนี้ได้
Oliver Charlesworth

6
@MohammadFadin: ลองดูที่เช่นen.wikipedia.org/wiki/Single_precisionและen.wikipedia.org/wiki/Unit_in_the_last_place
Oliver Charlesworth

1
ฉันไม่สามารถช่วยได้ แต่คิดว่าการแก้ไขนี้เป็นเพียงเครื่องสำอางเนื่องจากศูนย์จะปรากฏให้เห็นมากที่สุด ไม่ต้องสงสัยเลยว่ามีค่าอิงดัชนีจำนวนมากที่ได้รับผลกระทบจากข้อผิดพลาดในการปัดเศษ
Michaël Roy

232

สิ่งนี้ดูเหมือนจะเป็นข้อผิดพลาดที่รู้จัก ( Java bug 6430675: Math.round มีพฤติกรรมที่น่าประหลาดใจสำหรับ 0x1.fffffffffffffffp-2 ) ซึ่งได้รับการแก้ไขใน Java 7


5
+1: หาดี! ผูกกับความแตกต่างในเอกสารระหว่าง Java 6 และ 7 ตามที่อธิบายไว้ในคำตอบของฉัน
Oliver Charlesworth


83

ซอร์สโค้ดใน JDK 6:

public static long round(double a) {
    return (long)Math.floor(a + 0.5d);
}

ซอร์สโค้ดใน JDK 7:

public static long round(double a) {
    if (a != 0x1.fffffffffffffp-2) {
        // a is not the greatest double value less than 0.5
        return (long)Math.floor(a + 0.5d);
    } else {
        return 0;
    }
}

เมื่อค่าเป็น 0.49999999999999994d ใน JDK 6 จะเรียกใช้floorและดังนั้นจะส่งคืน 1 แต่ใน JDK 7 ifเงื่อนไขกำลังตรวจสอบว่าตัวเลขเป็นค่า double ที่ยิ่งใหญ่ที่สุดน้อยกว่า 0.5 หรือไม่ เช่นในกรณีนี้ตัวเลขจะไม่เป็นค่าคู่ที่ยิ่งใหญ่ที่สุดน้อยกว่า 0.5 ดังนั้นelseบล็อกจึงส่งคืน 0

คุณสามารถลอง 0.49999999999999999d ซึ่งจะคืนค่า 1 แต่ไม่ใช่ 0 เพราะนี่เป็นค่าสองครั้งที่ยิ่งใหญ่ที่สุดน้อยกว่า 0.5


1.499999999999999994 ต่อไปจะเกิดอะไรขึ้น ผลตอบแทน 2? มันควรส่งคืน 1 แต่คุณควรได้รับข้อผิดพลาดเดียวกันกับก่อนหน้านี้ แต่ใช้ 1. ?
mmm

6
1.499999999999999994 ไม่สามารถแสดงในทศนิยมที่มีความแม่นยำสองเท่า 1.4999999999999998 เป็นคู่ที่เล็กที่สุดน้อยกว่า 1.5 อย่างที่คุณเห็นจากคำถามfloorวิธีรอบมันอย่างถูกต้อง
OrangeDog

26

ฉันได้รับเหมือนกันใน JDK 1.6 32 บิต แต่บน Java 7 64- บิตฉันมี 0 สำหรับ 0.49999999999999994 ซึ่งปัดเป็น 0 และบรรทัดสุดท้ายไม่พิมพ์ ดูเหมือนว่าจะเป็นปัญหาของ VM อย่างไรก็ตามหากใช้จุดลอยตัวคุณควรคาดหวังว่าผลลัพธ์จะแตกต่างกันเล็กน้อยในสภาพแวดล้อมต่างๆ (CPU, โหมด 32- หรือ 64- บิต)

และเมื่อใช้roundหรือย้อนกลับเมทริกซ์ ฯลฯบิตเหล่านี้สามารถสร้างความแตกต่างอย่างมาก

เอาต์พุต x64:

10.5 rounded is 11
10.499999999999998 rounded is 10
9.5 rounded is 10
9.499999999999998 rounded is 9
8.5 rounded is 9
8.499999999999998 rounded is 8
7.5 rounded is 8
7.499999999999999 rounded is 7
6.5 rounded is 7
6.499999999999999 rounded is 6
5.5 rounded is 6
5.499999999999999 rounded is 5
4.5 rounded is 5
4.499999999999999 rounded is 4
3.5 rounded is 4
3.4999999999999996 rounded is 3
2.5 rounded is 3
2.4999999999999996 rounded is 2
1.5 rounded is 2
1.4999999999999998 rounded is 1
0.5 rounded is 1
0.49999999999999994 rounded is 0

ใน Java 7 (เวอร์ชันที่คุณใช้ทดสอบ) ข้อผิดพลาดได้รับการแก้ไข
IvánPérez

1
ฉันคิดว่าคุณหมายถึง 32 บิต ฉันสงสัยen.wikipedia.org/wiki/ZEBRA_%28computer%29สามารถเรียกใช้ Java และฉันสงสัยว่ามีเครื่อง 33 บิตตั้งแต่นั้นมา
chx

@chx ค่อนข้างเห็นได้ชัดเพราะผมเคยเขียน 32 บิตก่อน :)
Danubian เซเลอร์

11

คำตอบต่อจากนี้เป็นข้อความที่ตัดตอนมาจากรายงานข้อผิดพลาดของOracle 6430,675ที่ เยี่ยมชมรายงานสำหรับคำอธิบายแบบเต็ม

วิธีการทางคณิตศาสตร์ {, StrictMath.round มีการกำหนดเป็น

(long)Math.floor(a + 0.5d)

สำหรับอาร์กิวเมนต์สองครั้ง ในขณะที่คำจำกัดความนี้มักจะทำงานได้ตามที่คาดไว้ แต่ก็ให้ผลลัพธ์ที่น่าประหลาดใจที่ 1 แทนที่จะเป็น 0 สำหรับ 0x1.fffffffffffffffp-2 (0.49999999999999994)

ค่า 0.49999999999999994 เป็นค่าจุดลอยตัวที่ยิ่งใหญ่ที่สุดน้อยกว่า 0.5 ในฐานะที่เป็นเลขฐานสิบหกทศนิยมตามตัวอักษรค่าของมันคือ 0x1.fffffffffffffffp-2 ซึ่งเท่ากับ (2 - 2 ^ 52) * 2 ^ -2 == (0.5 - 2 ^ 54) ดังนั้นมูลค่าที่แน่นอนของผลรวม

(0.5 - 2^54) + 0.5

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

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

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