Math.round (0.49999999999999994) neden 1 döndürüyor?


567

Aşağıdaki programda, her bir değerin .5, dışında, aşağı doğru yuvarlandığını görebilirsiniz 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);
}

baskılar

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 güncellemesini kullanıyorum.


1
Java 1.7.0'da
Kahve

2
@Adel: Oli'nin cevabı hakkındaki yorumuma bakın , Java 6'nın bunu (ve yaptığı belgeleri ) 0.5numaraya ekleyip daha sonra kullanarak daha fazla hassasiyet kaybına neden olacak şekilde uyguladığı anlaşılıyor floor; Java 7 artık bu şekilde belgelememektedir (muhtemelen / umarım düzelttiklerinden dolayı).
TJ Crowder

1
Yazdığım bir test programında bir hataydı. ;)
Peter Lawrey

1
Kayan nokta değerlerini gösteren başka bir örnek, yüz değerinden alınamaz.
Michaël Roy

1
Bunu düşündükten sonra. Bir sorun görmüyorum. 0.49999999999999994, 0,5'ten küçük, temsil edilebilen en küçük sayıdan daha büyüktür ve ondalık okunabilir formdaki temsilin kendisi , bizi kandırmaya çalışan bir yaklaşımdır .
Michaël Roy

Yanıtlar:


574

özet

Java 6'da (ve muhtemelen daha önce), round(x)olarak uygulanır floor(x+0.5). 1 Bu bir spesifikasyon hatasıdır, tam da bu patolojik durum için. 2 Java 7 artık bu bozuk uygulamayı zorunlu kılmamaktadır. 3

Sorun

0.5 + 0.49999999999999994 çift hassasiyette tam olarak 1'dir:

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
}

Bunun nedeni, 0.49999999999999994'ün 0,5'ten daha küçük bir üssüne sahip olmasıdır, bu nedenle eklendiklerinde mantis kaydırılır ve ULP büyür.

Çözüm

Java 7'den beri, OpenJDK (örneğin) bunu uygular: 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 (bunu bulmak için @SimonNickerson'a kredi)

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


Ben bu tanımı görmüyorum roundiçinde için JavadocMath.round veya genel bakışında Mathsınıfında.
TJ Crowder

3
@ Oli: Ah, bu ilginç, Java 7 (bağlandığım dokümanlar) için bunu biraz çıkardılar - belki de (daha fazla) bir hassasiyet kaybını tetikleyerek bu tür garip davranışlara neden olmaktan kaçınmak için.
TJ Crowder

@TJCrowder: Evet, ilginç. Bu varsayımı doğrulayabilmemiz için tek tek Java sürümleri için herhangi bir "sürüm notu" / "iyileştirme" belgesi olup olmadığını biliyor musunuz?
Oliver Charlesworth


1
Sıfırın en görünür olduğu için bu düzeltmenin sadece kozmetik olduğunu düşünemiyorum. Bu yuvarlama hatasından etkilenen başka pek çok kayan nokta değeri şüphesizdir.
Michaël Roy


83

JDK 6'daki kaynak kodu:

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

JDK 7'deki kaynak kodu:

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;
    }
}

Değer 0.49999999999999994d olduğunda, JDK 6'da kat çağırır ve bu nedenle 1 değerini döndürür, ancak JDK 7'de ifkoşul, sayının 0,5'ten küçük en büyük çift değer olup olmadığını kontrol eder. Bu durumda, sayı 0,5'ten küçük en büyük çift değer değildir, bu nedenle elseblok 0 döndürür.

0 değerini döndürmeyecek 0,49999999999999999d'yi deneyebilirsiniz, çünkü bu 0,5'ten az olan en büyük çift değerdir.


1.499999999999999994 burada ne olacak? 2 döndürür? 1 döndürmelidir, ancak bu daha önce olduğu gibi aynı hatayı almalı, ancak 1 ile.?
mmm

6
1.499999999999999994 çift kesinlikli kayar nokta ile temsil edilemez. 1.4999999999999998, 1.5'ten küçük en küçük çifttir. Sorudan da görebileceğiniz gibi, flooryöntem onu ​​doğru bir şekilde yuvarlar.
OrangeDog

26

Aynı şeyi JDK 1.6 32-bit'te aldım, ancak Java 7 64-bit'te 0.49999999999999994 için 0 aldım ve yuvarlak olan 0 ve son satır yazdırılmadı. Bir VM sorunu gibi görünüyor, ancak kayan noktaları kullanarak sonuçların çeşitli ortamlarda (CPU, 32 veya 64 bit modu) biraz farklı olmasını beklemelisiniz.

Ve roundmatrisleri vb. Kullanırken veya ters çevirirken, bu bitler büyük bir fark yaratabilir.

x64 çıkışı:

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'de (test etmek için kullandığınız sürüm) hata düzeltildi.
Iván Pérez

1
Bence 32 bit demek istedin. Şüphe en.wikipedia.org/wiki/ZEBRA_%28computer%29 Java çalıştırabilir ve o zamandan beri bir 33 bit makine olmuştur şüphesiz.
chx

@chx oldukça açık, çünkü daha önce 32 bit yazdım :)
Danubian Sailor

11

Bundan sonraki cevap 6430675 numaralı Oracle hata raporunun bir alıntısıdır . Tam açıklama için raporu ziyaret edin.

{Math, StrictMath.round yöntemleri işlevsel olarak şu şekilde tanımlanır:

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

çifte argümanlar için. Bu tanım genellikle beklendiği gibi çalışsa da, 0x1.fffffffffffffp-2 (0.49999999999999994) için 0 yerine 1'in şaşırtıcı sonucunu verir.

0.49999999999999994 değeri 0.5'ten küçük en büyük kayar nokta değeridir. Onaltılık kayan nokta değişmez değeri olarak değeri 0x1.fffffffffffffp-2'dir ve bu değer (2 - 2 ^ 52) * 2 ^ -2'ye eşittir. == (0,5 - 2 ^ 54). Bu nedenle, toplamın kesin değeri

(0.5 - 2^54) + 0.5

1 - 2 ^ 54'tür. Bu, bitişik iki kayan nokta sayısı (1 - 2 ^ 53) ve 1'in ortasındadır. IEEE 754 aritmetik turunda Java tarafından kullanılan en yakın çift yuvarlama moduna, kayan nokta sonuçları kesin olmadığında, ikisine daha yakın kesin sonucu döndüren temsili kayan nokta değerleri döndürülmelidir; her iki değer de eşit derecede yakınsa, son bit sıfırı döndürülen değer döndürülür. Bu durumda, eklemeden doğru dönüş değeri 1'dir, en büyük değer 1'den küçük değildir.

Yöntem tanımlandığı gibi çalışırken, bu girdideki davranış çok şaşırtıcıdır; spesifikasyon "bu girişteki davranışın değiştirilmesine izin verecek olan" En yakın uzunluğa yuvarla, yuvarlama bağlarına yuvarla "gibi bir şeye değiştirilebilir.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.