Tại sao Math.round (0.49999999999999994) trả về 1?


567

Trong chương trình sau, bạn có thể thấy rằng mỗi giá trị nhỏ hơn một chút .5được làm tròn xuống, ngoại trừ 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);
}

in

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

Tôi đang sử dụng bản cập nhật Java 6 31.


1
Trên java 1.7.0, nó hoạt động tốt i.imgur.com/hZeqx.png
Cà phê

2
@Adel: Xem nhận xét của tôi về câu trả lời của Oli , có vẻ như Java 6 thực hiện điều này (và các tài liệu mà nó làm ) theo cách có thể làm mất thêm độ chính xác bằng cách thêm 0.5vào số và sau đó sử dụng floor; Java 7 không còn tài liệu theo cách đó (có lẽ / hy vọng vì họ đã sửa nó).
TJ Crowder

1
Đó là một lỗi trong một chương trình thử nghiệm tôi đã viết. ;)
Peter Lawrey

1
Một ví dụ khác cho thấy các giá trị dấu phẩy động không thể được lấy theo mệnh giá.
Michaël Roy

1
Sau khi nghĩ về nó. Tôi không thấy một vấn đề. 0.49999999999999994 lớn hơn số đại diện nhỏ nhất nhỏ hơn 0,5 và biểu diễn ở dạng thập phân có thể đọc được của con người tự nó là một xấp xỉ đang cố gắng đánh lừa chúng ta.
Michaël Roy

Câu trả lời:


574

Tóm lược

Trong Java 6 (và có lẽ sớm hơn), round(x)được triển khai như floor(x+0.5). 1 Đây là một lỗi đặc tả, chính xác là một trường hợp bệnh lý này. 2 Java 7 không còn bắt buộc thực hiện bị hỏng này. 3

Vấn đề

0,5 + 0,49999999999999994 chính xác là 1 trong độ chính xác kép:

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
}

Điều này là do 0.49999999999999994 có số mũ nhỏ hơn 0,5, vì vậy khi chúng được thêm vào, lớp phủ của nó bị dịch chuyển và ULP trở nên lớn hơn.

Giải pháp

Kể từ Java 7, OpenJDK (ví dụ) triển khai nó như vậy: 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/orpdatabase/view_orms.do?orms_id=6430675 (tín dụng cho @SimonNickerson để tìm thấy điều này)

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

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


Tôi không thấy định nghĩa đó roundtrong Javadoc choMath.round hoặc trong tổng quan về Mathlớp.
TJ Crowder

3
@ Oli: Ồ bây giờ thật thú vị, họ đã lấy bit đó ra cho Java 7 (các tài liệu tôi đã liên kết) - có thể để tránh gây ra loại hành vi kỳ quặc này bằng cách gây ra sự mất độ chính xác (hơn nữa).
TJ Crowder

@TJCrowder: Vâng, thật thú vị. Bạn có biết liệu có bất kỳ loại tài liệu "ghi chú phát hành" / "cải tiến" nào cho các phiên bản Java riêng lẻ không, để chúng tôi có thể xác minh giả định này?
Oliver Charlesworth


1
Tôi không thể không nghĩ rằng sửa chữa này chỉ là mỹ phẩm, vì số 0 là rõ ràng nhất. Không có nghi ngờ gì nhiều giá trị dấu phẩy động khác bị ảnh hưởng bởi lỗi làm tròn này.
Michaël Roy

232

Đây dường như là một lỗi đã biết (lỗi Java 6430675: Math.round có hành vi đáng ngạc nhiên đối với 0x1.fffffffffffffffp-2 ) đã được sửa trong Java 7.


5
+1: Tìm thấy tốt đẹp! Liên kết với sự khác biệt trong tài liệu giữa Java 6 và 7 như được giải thích trong câu trả lời của tôi.
Oliver Charlesworth


83

Mã nguồn trong JDK 6:

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

Mã nguồn trong 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;
    }
}

Khi giá trị là 0,49999999999999994d, trong JDK 6, nó sẽ gọi sàn và trả về 1, nhưng trong JDK 7, ifđiều kiện là kiểm tra xem số đó có phải là giá trị kép lớn nhất nhỏ hơn 0,5 hay không. Như trong trường hợp này, số không phải là giá trị kép lớn nhất nhỏ hơn 0,5, do đó, elsekhối trả về 0.

Bạn có thể thử 0.49999999999999999d, sẽ trả về 1, nhưng không trả về 0, vì đây là giá trị kép lớn nhất nhỏ hơn 0,5.


Điều gì xảy ra với 1.499999999999999994 ở đây? trả về 2? nó sẽ trả về 1, nhưng điều này sẽ khiến bạn gặp lỗi tương tự như trước đó, nhưng với 1.?
mmm

6
1.499999999999999994 không thể được biểu diễn ở điểm nổi chính xác kép. 1.4999999999999998 là số nhỏ nhất nhỏ hơn 1,5. Như bạn có thể thấy từ câu hỏi, floorphương pháp làm tròn nó một cách chính xác.
OrangeDog

26

Tôi đã nhận được điều tương tự trên JDK 1.6 32 bit, nhưng trên Java 7 64 bit tôi đã nhận được 0 với giá 0,49999999999999994 được làm tròn là 0 và dòng cuối cùng không được in. Có vẻ như đây là sự cố VM, tuy nhiên, bằng cách sử dụng các dấu phẩy động, bạn nên mong đợi kết quả khác nhau một chút trên các môi trường khác nhau (CPU, chế độ 32 hoặc 64 bit).

Và, khi sử dụng roundhoặc đảo ngược ma trận, v.v., các bit này có thể tạo ra sự khác biệt rất lớn.

đầu ra 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

Trong Java 7 (phiên bản bạn đang sử dụng để kiểm tra) lỗi đã được sửa.
Iván Pérez

1
Tôi nghĩ bạn có nghĩa là 32 bit. Tôi nghi ngờ en.wikipedia.org/wiki/ZEBRA_%28computer%29 có thể chạy Java và tôi nghi ngờ đã có một máy 33 bit kể từ đó.
chx

@chx khá rõ ràng, vì tôi đã viết 32 bit trước đó :)
Thủy thủ Danubian

11

Câu trả lời sau đây là một đoạn trích của báo cáo lỗi Oracle 6430675 tại. Truy cập báo cáo để được giải thích đầy đủ.

Các phương thức {Math, StrictMath.round được định nghĩa hoạt động là

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

cho các đối số kép. Mặc dù định nghĩa này thường hoạt động như mong đợi, nhưng nó mang lại kết quả đáng ngạc nhiên là 1, thay vì 0, cho 0x1.fffffffffffffffp-2 (0.49999999999999994).

Giá trị 0.49999999999999994 là giá trị dấu phẩy động lớn nhất nhỏ hơn 0,5. Là một dấu phẩy động thập lục phân, giá trị của nó là 0x1.fffffffffffffp-2, bằng (2 - 2 ^ 52) * 2 ^ -2. == (0,5 - 2 ^ 54). Do đó, giá trị chính xác của tổng

(0.5 - 2^54) + 0.5

là 1 - 2 ^ 54. Đây là một nửa giữa hai số dấu phẩy động liền kề (1 - 2 ^ 53) và 1. Trong vòng số học của IEEE 754 đến chế độ làm tròn chẵn gần nhất được sử dụng bởi Java, khi kết quả của dấu phẩy động không chính xác, càng gần hai các giá trị dấu phẩy động có thể biểu diễn mà khung kết quả chính xác phải được trả về; nếu cả hai giá trị gần bằng nhau, giá trị bit 0 cuối cùng của nó được trả về. Trong trường hợp này, giá trị trả về chính xác từ add là 1, không phải giá trị lớn nhất nhỏ hơn 1.

Trong khi phương thức đang hoạt động như được định nghĩa, hành vi trên đầu vào này rất đáng ngạc nhiên; đặc điểm kỹ thuật có thể được sửa đổi thành một cái gì đó giống như "Làm tròn đến các mối quan hệ làm tròn dài nhất gần nhất", điều này sẽ cho phép thay đổi hành vi trên đầu vào này.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.