Mengapa Math.round (0.49999999999999994)) mengembalikan 1?

567

Dalam program berikut ini Anda dapat melihat bahwa setiap nilai sedikit kurang dari .5dibulatkan, kecuali untuk 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);
}

cetakan

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

Saya menggunakan pembaruan Java 6 31.

Peter Lawrey
sumber
1
Di java 1.7.0 berfungsi ok i.imgur.com/hZeqx.png
Coffee
2
@Adel: Lihat komentar saya pada jawaban Oli , sepertinya Java 6 mengimplementasikan ini (dan mendokumentasikannya ) dengan cara yang dapat menyebabkan hilangnya presisi lebih lanjut dengan menambahkan 0.5nomor dan kemudian menggunakan floor; Java 7 tidak lagi mendokumentasikannya seperti itu (mungkin / semoga karena mereka memperbaikinya).
TJ Crowder
1
Itu adalah bug dalam program pengujian yang saya tulis. ;)
Peter Lawrey
1
Contoh lain yang menunjukkan nilai floating point tidak dapat diambil pada nilai nominal.
Michaël Roy
1
Setelah memikirkannya. Saya tidak melihat masalah. 0,49999999999999994 lebih besar dari angka terkecil yang dapat diwakili kurang dari 0,5, dan representasi dalam bentuk desimal yang dapat dibaca manusia itu sendiri merupakan perkiraan yang mencoba membodohi kita.
Michaël Roy

Jawaban:

574

Ringkasan

Di Jawa 6 (dan mungkin sebelumnya), round(x)diimplementasikan sebagai floor(x+0.5). 1 Ini adalah bug spesifikasi, untuk kasus patologis yang tepat ini. 2 Java 7 tidak lagi mengamanatkan implementasi yang rusak ini. 3

Masalah

0,5 + 0,49999999999999994 tepat 1 dalam presisi ganda:

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
}

Ini karena 0.4999999999999999994 memiliki eksponen yang lebih kecil dari 0,5, jadi ketika mereka ditambahkan, mantissa-nya berubah, dan ULP semakin besar.

Solusinya

Sejak Java 7, OpenJDK (misalnya) mengimplementasikannya sebagai berikut: 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 (kredit ke @SimonNickerson karena menemukan ini)

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

Oliver Charlesworth
sumber
Saya tidak melihat definisi rounddi Javadoc untukMath.round atau dalam ikhtisar Mathkelas.
TJ Crowder
3
@ Oli: Oh sekarang itu menarik, mereka mengambil bit itu untuk Java 7 (dokumen yang saya tautkan) - mungkin untuk menghindari menyebabkan perilaku aneh semacam ini dengan memicu (lebih jauh) kehilangan presisi.
TJ Crowder
@TJCrowder: Ya, ini menarik. Apakah Anda tahu jika ada dokumen "rilis catatan" / "perbaikan" untuk masing-masing versi Java, sehingga kami dapat memverifikasi asumsi ini?
Oliver Charlesworth
6
@MohammadFadin: Lihatlah misalnya en.wikipedia.org/wiki/Single_precision dan en.wikipedia.org/wiki/Unit_in_the_last_place .
Oliver Charlesworth
1
Saya tidak dapat membantu tetapi berpikir bahwa perbaikan ini hanya kosmetik, karena nol paling terlihat. Tidak ada keraguan banyak nilai floating point lain yang dipengaruhi oleh kesalahan pembulatan ini.
Michaël Roy
83

Kode sumber dalam JDK 6:

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

Kode sumber di 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;
    }
}

Ketika nilainya 0,4999999999999999994d, dalam JDK 6, itu akan memanggil lantai dan karenanya mengembalikan 1, tetapi dalam JDK 7, ifkondisinya sedang memeriksa apakah angka tersebut adalah nilai ganda terbesar kurang dari 0,5 atau tidak. Seperti dalam kasus ini angkanya bukan nilai ganda terbesar kurang dari 0,5, sehingga elseblok mengembalikan 0.

Anda dapat mencoba 0,49999999999999999d, yang akan mengembalikan 1, tetapi bukan 0, karena ini adalah nilai ganda terbesar kurang dari 0,5.

Chandra Sekhar
sumber
apa yang terjadi dengan 1.499999999999999994 di sini? mengembalikan 2? seharusnya mengembalikan 1, tetapi ini harus membuat Anda kesalahan yang sama seperti sebelumnya, tetapi dengan 1.?
mmm
6
1.499999999999999994 tidak dapat direpresentasikan dalam floating-point presisi ganda. 1.4999999999999998 adalah dobel terkecil kurang dari 1,5. Seperti yang Anda lihat dari pertanyaan, floormetode membulatkannya dengan benar.
OrangeDog
26

Saya mendapatkan hal yang sama pada JDK 1.6 32-bit, tetapi pada Java 7 64-bit saya mendapatkan 0 untuk 0.4999999999999999994 yang dibulatkan adalah 0 dan baris terakhir tidak dicetak. Tampaknya menjadi masalah VM, namun, menggunakan floating point, Anda harus mengharapkan hasilnya sedikit berbeda pada berbagai lingkungan (CPU, mode 32- atau 64-bit).

Dan, ketika menggunakan roundatau membalikkan matriks, dll., Bit ini dapat membuat perbedaan besar.

output 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
Pelaut Danubia
sumber
Di Java 7 (versi yang Anda gunakan untuk mengujinya) bug diperbaiki.
Iván Pérez
1
Saya pikir Anda berarti 32 bit. Saya ragu en.wikipedia.org/wiki/ZEBRA_%28computer%29 dapat menjalankan Java dan saya ragu telah ada mesin 33 bit sejak itu.
chx
@ chx cukup jelas, karena saya telah menulis 32 bit sebelumnya :)
Danubian Sailor
11

Jawaban selanjutnya adalah kutipan dari laporan bug Oracle di 6430675 . Kunjungi laporan untuk penjelasan lengkap.

Metode {Math, StrictMath.round secara operasional didefinisikan sebagai

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

untuk argumen ganda. Sementara definisi ini biasanya berfungsi seperti yang diharapkan, ini memberikan hasil yang mengejutkan dari 1, bukan 0, untuk 0x1.fffffffffffffp-2 (0.4999999999999999994).

Nilai 0,49999999999999994 adalah nilai floating-point terbesar kurang dari 0,5. Sebagai heksadesimal floating-point literal nilainya adalah 0x1.fffffffffffffp-2, yang sama dengan (2 - 2 ^ 52) * 2 ^ -2. == (0,5 - 2 ^ 54). Oleh karena itu, nilai penjumlahannya tepat

(0.5 - 2^54) + 0.5

adalah 1 - 2 ^ 54. Ini adalah setengah antara dua angka floating-point yang berdekatan (1 - 2 ^ 53) dan 1. Dalam putaran aritmetika IEEE 754 ke mode pembulatan genap terdekat yang digunakan oleh Java, ketika hasil floating-point tidak tepat, semakin dekat keduanya nilai floating-point yang dapat diwakili yang mengurung hasil yang tepat harus dikembalikan; jika kedua nilai sama-sama dekat, nilai bit nol terakhirnya dikembalikan. Dalam hal ini nilai balik yang benar dari tambah adalah 1, bukan nilai terbesar kurang dari 1.

Sementara metode beroperasi sebagaimana didefinisikan, perilaku pada input ini sangat mengejutkan; spesifikasi dapat diubah menjadi sesuatu yang lebih seperti "Putaran ke panjang terdekat, ikatan pembulatan ke atas," yang akan memungkinkan perilaku pada input ini diubah.

shiv.mymail
sumber