Tại sao thêm 0,1 nhiều lần vẫn không mất?


152

Tôi biết 0.1số thập phân không thể được biểu diễn chính xác bằng số nhị phân hữu hạn ( giải thích ), do đó double n = 0.1sẽ mất một số độ chính xác và sẽ không chính xác 0.1. Mặt khác 0.5có thể được đại diện chính xác bởi vì nó là 0.5 = 1/2 = 0.1b.

Phải nói rằng việc thêm 0.1 ba lần sẽ không chính xác 0.3để in mã sau đây false:

double sum = 0, d = 0.1;
for (int i = 0; i < 3; i++)
    sum += d;
System.out.println(sum == 0.3); // Prints false, OK

Nhưng làm thế nào mà thêm 0.1 năm lần sẽ cho chính xác 0.5? Các mã sau in true:

double sum = 0, d = 0.1;
for (int i = 0; i < 5; i++)
    sum += d;
System.out.println(sum == 0.5); // Prints true, WHY?

Nếu 0.1không thể được biểu diễn chính xác, làm thế nào mà việc thêm 5 lần lại cho chính xác 0.5có thể được biểu diễn chính xác?


7
Nếu bạn thực sự nghiên cứu thì tôi chắc chắn bạn có thể tìm ra nó, nhưng điểm nổi chứa đầy "sự ngạc nhiên", và đôi khi tốt hơn là chỉ nhìn vào trong ngạc nhiên.
Licks nóng

3
Bạn đang nghĩ về điều này một cách toán học. Mỹ phẩm điểm nổi không phải là toán học theo bất kỳ cách nào.
Jakob

13
@HotLicks đó là rất nhiều thái độ sai lầm để có.
hobbs

2
@RussellBorogove ngay cả khi nó được tối ưu hóa, nó sẽ chỉ là một tối ưu hóa hợp lệ nếu sumcó cùng giá trị cuối cùng như thể vòng lặp thực sự được thực thi. Trong tiêu chuẩn C ++, điều này được gọi là "quy tắc as-if" hoặc "cùng một hành vi có thể quan sát được".
hobbs

7
@Jakob không đúng chút nào. Số học dấu phẩy động được xác định chặt chẽ, với việc xử lý tốt các giới hạn lỗi và như vậy. Chỉ là nhiều lập trình viên không sẵn lòng theo dõi phân tích, hoặc họ nhầm tưởng rằng "dấu phẩy động là không chính xác" là tất cả những gì cần biết và phân tích đó không đáng bận tâm.
hobbs

Câu trả lời:


155

Lỗi làm tròn không phải là ngẫu nhiên và cách nó được thực hiện, nó cố gắng giảm thiểu lỗi. Điều này có nghĩa là đôi khi lỗi không hiển thị hoặc không có lỗi.

Ví dụ 0.1không chính xác 0.1tức là new BigDecimal("0.1") < new BigDecimal(0.1)nhưng 0.5chính xác1.0/2

Chương trình này cho bạn thấy các giá trị thực sự liên quan.

BigDecimal _0_1 = new BigDecimal(0.1);
BigDecimal x = _0_1;
for(int i = 1; i <= 10; i ++) {
    System.out.println(i+" x 0.1 is "+x+", as double "+x.doubleValue());
    x = x.add(_0_1);
}

in

0.1000000000000000055511151231257827021181583404541015625, as double 0.1
0.2000000000000000111022302462515654042363166809082031250, as double 0.2
0.3000000000000000166533453693773481063544750213623046875, as double 0.30000000000000004
0.4000000000000000222044604925031308084726333618164062500, as double 0.4
0.5000000000000000277555756156289135105907917022705078125, as double 0.5
0.6000000000000000333066907387546962127089500427246093750, as double 0.6000000000000001
0.7000000000000000388578058618804789148271083831787109375, as double 0.7000000000000001
0.8000000000000000444089209850062616169452667236328125000, as double 0.8
0.9000000000000000499600361081320443190634250640869140625, as double 0.9
1.0000000000000000555111512312578270211815834045410156250, as double 1.0

Lưu ý: đó 0.3là hơi tắt, nhưng khi bạn nhận được 0.4các bit phải chuyển xuống một để phù hợp với giới hạn 53 bit và lỗi được loại bỏ. Một lần nữa, một lỗi creep sao trong cho 0.60.7nhưng đối 0.8với 1.0các lỗi được loại bỏ.

Thêm 5 lần nên tích lũy lỗi, không hủy được.

Lý do có lỗi là do độ chính xác hạn chế. tức là 53 bit. Điều này có nghĩa là khi số lượng sử dụng nhiều bit hơn khi nó lớn hơn, các bit phải được loại bỏ ở cuối. Điều này gây ra làm tròn mà trong trường hợp này là có lợi cho bạn.
Bạn có thể nhận được hiệu ứng ngược lại khi nhận được một số nhỏ hơn, ví dụ 0.1-0.0999=> 1.0000000000000286E-4 và bạn thấy nhiều lỗi hơn trước.

Một ví dụ về điều này là tại sao trong Java 6 Tại sao Math.round (0.49999999999999994) trả về 1 Trong trường hợp này, việc mất một chút trong tính toán dẫn đến sự khác biệt lớn cho câu trả lời.


1
Điều này được thực hiện ở đâu?
EpicPandaForce

16
@Zhuinden CPU tuân theo tiêu chuẩn IEEE-754. Java cung cấp cho bạn quyền truy cập vào các hướng dẫn CPU cơ bản và không tham gia. vi.wikipedia.org/wiki/IEEE_floating_point
Peter Lawrey

10
@PeterLawrey: Không nhất thiết phải là CPU. Trên một máy không có điểm nổi trong CPU (và không sử dụng FPU riêng), số học của IEEE sẽ được thực hiện bằng phần mềm. Và nếu CPU chủ có điểm nổi nhưng không phù hợp với yêu cầu của IEEE, tôi nghĩ rằng việc triển khai Java cho CPU đó cũng bắt buộc phải sử dụng float mềm ...
R .. GitHub DỪNG GIÚP ICE

1
@R .. trong trường hợp tôi không biết điều gì sẽ xảy ra nếu bạn sử dụng strictfp Thời gian để xem xét các số nguyên điểm cố định tôi nghĩ. (hoặc BigDecimal)
Peter Lawrey

2
@eugene vấn đề chính là điểm nổi giá trị giới hạn có thể biểu thị. Hạn chế này có thể dẫn đến mất thông tin và khi số lượng tăng lên mất lỗi. Nó sử dụng làm tròn nhưng trong trường hợp này, làm tròn xuống để số có thể là một số quá lớn vì 0,1 là hơi quá lớn, biến thành giá trị chính xác. Chính xác 0,5
Peter Lawrey

47

Tràn tràn, trong dấu phẩy động, x + x + xchính xác là số dấu phẩy động được làm tròn chính xác (nghĩa là gần nhất) với 3 * thực x, x + x + x + xchính xác là 4 * x, và x + x + x + x + xmột lần nữa là xấp xỉ điểm nổi được làm tròn chính xác cho 5 * x.

Kết quả đầu tiên, cho x + x + x, xuất phát từ thực tế x + xlà chính xác. x + x + xdo đó là kết quả của chỉ làm tròn một.

Kết quả thứ hai là khó khăn hơn, một minh chứng về nó được thảo luận ở đây (và Stephen Canon ám chỉ đến một bằng chứng khác bằng phân tích trường hợp trên 3 chữ số cuối của x). Tóm lại, 3 * xnằm trong cùng một binade với 2 * xhoặc trong cùng một binade với 4 * x, và trong mỗi trường hợp, có thể suy ra rằng lỗi trong lần bổ sung thứ ba sẽ hủy bỏ lỗi trong lần bổ sung thứ hai ( bổ sung đầu tiên là chính xác, như chúng ta đã nói).

Kết quả thứ ba, x + x + x + x + xLv được làm tròn chính xác, xuất phát từ lần thứ hai giống như cách đầu tiên xuất phát từ tính chính xác của x + x.


Kết quả thứ hai giải thích tại sao 0.1 + 0.1 + 0.1 + 0.1chính xác là số dấu phẩy động 0.4: các số hữu tỷ 1/10 và 4/10 được xấp xỉ theo cùng một cách, với cùng một lỗi tương đối, khi được chuyển đổi thành dấu phẩy động. Các số dấu phẩy động này có tỷ lệ chính xác là 4 giữa chúng. Kết quả thứ nhất và thứ ba cho thấy 0.1 + 0.1 + 0.10.1 + 0.1 + 0.1 + 0.1 + 0.1có thể có ít lỗi hơn so với phân tích lỗi ngây thơ, nhưng, bản thân chúng chỉ liên quan đến kết quả tương ứng 3 * 0.15 * 0.1có thể được dự kiến ​​là gần nhưng không nhất thiết phải giống hệt 0.30.5.

Nếu bạn tiếp tục thêm 0.1sau lần bổ sung thứ tư, cuối cùng bạn sẽ quan sát thấy các lỗi làm tròn khiến 0.1cho nó được thêm vào chính nó lần n lần chuyển hướng từ n * 0.1và chuyển hướng thậm chí nhiều hơn từ n / 10. Nếu bạn định vẽ các giá trị của Hồi 0,1 được thêm vào chính nó lần n là một hàm của n, bạn sẽ quan sát các đường có độ dốc không đổi bằng các nhị phân (ngay khi kết quả của phép cộng thứ n được định sẵn rơi vào một nhóm nhị phân cụ thể, các thuộc tính của phần bổ sung có thể được dự kiến ​​là tương tự như các phần bổ sung trước đó tạo ra kết quả trong cùng một binade). Trong cùng một binade, lỗi sẽ tăng hoặc thu hẹp. Nếu bạn nhìn vào chuỗi các sườn dốc từ binade sang binade, bạn sẽ nhận ra các chữ số lặp lại của0.1trong nhị phân một thời gian. Sau đó, sự hấp thụ sẽ bắt đầu diễn ra và đường cong sẽ đi bằng phẳng.


1
Trên dòng đầu tiên bạn nói rằng x + x + x là chính xác, nhưng từ ví dụ trong câu hỏi thì không.
Alboz

2
@Alboz Tôi nói rằng đó x + x + xchính xác là số dấu phẩy động được làm tròn chính xác đến 3 * thực x. Một cách chính xác, làm tròn một cách chính xác, có nghĩa là những người gần đây nhất trong bối cảnh này.
Pascal Cuoq

4
+1 Đây phải là câu trả lời được chấp nhận. Nó thực sự cung cấp giải thích / bằng chứng về những gì đang xảy ra thay vì chỉ chung chung mơ hồ.
R .. GitHub DỪNG GIÚP ICE

1
@Alboz (tất cả đều được hình dung bằng câu hỏi). Nhưng những gì câu trả lời này giải thích là làm thế nào các lỗi hủy bỏ một cách tình cờ thay vì thêm vào một trường hợp xấu nhất.
hobbs

1
@chebus 0,1 là 0x1.999999999999999999999 từ p-4 theo hệ thập lục phân (một chuỗi các chữ số vô hạn). Nó gần đúng với độ chính xác kép là 0x1.99999ap-4. 0,2 là 0x1.999999999999999999999 p-3 ở dạng thập lục phân. Vì lý do tương tự, 0,1 xấp xỉ bằng 0x1.99999ap-4, 0.2 xấp xỉ bằng 0x1.99999ap-3. Trong khi đó, 0x1.99999ap-3 cũng chính xác là 0x1.99999ap-4 + 0x1.99999ap-4.
Pascal Cuoq

-1

Các hệ thống dấu phẩy động thực hiện nhiều phép thuật khác nhau bao gồm có thêm một vài độ chính xác để làm tròn. Do đó, lỗi rất nhỏ do biểu diễn không chính xác của 0,1 kết thúc bị làm tròn thành 0,5.

Hãy nghĩ về dấu phẩy động là một cách tuyệt vời nhưng INEXACT để thể hiện các con số. Không phải tất cả các số có thể được dễ dàng đại diện trong một máy tính. Số vô tỷ như PI. Hoặc như SQRT (2). (Các hệ thống toán học tượng trưng có thể đại diện cho chúng, nhưng tôi đã nói "một cách dễ dàng".)

Giá trị dấu phẩy động có thể cực kỳ gần, nhưng không chính xác. Nó có thể rất gần đến nỗi bạn có thể điều hướng đến Sao Diêm Vương và cách xa từng milimet. Nhưng vẫn không chính xác theo nghĩa toán học.

Không sử dụng dấu phẩy động khi bạn cần chính xác hơn là gần đúng. Ví dụ, các ứng dụng kế toán muốn theo dõi chính xác một số đồng xu nhất định trong tài khoản. Số nguyên là tốt cho điều đó bởi vì chúng là chính xác. Vấn đề chính bạn cần theo dõi với số nguyên là tràn.

Sử dụng BigDecimal cho tiền tệ hoạt động tốt vì đại diện cơ bản là một số nguyên, mặc dù là một số lớn.

Nhận ra rằng các số dấu phẩy động là không chính xác, chúng vẫn có rất nhiều công dụng. Hệ thống tọa độ để điều hướng hoặc tọa độ trong hệ thống đồ họa. Giá trị thiên văn. Giá trị khoa học. (Có lẽ bạn không thể biết chính xác khối lượng của một quả bóng chày trong khối lượng của một electron, vì vậy sự thiếu chính xác không thực sự quan trọng.)

Để đếm các ứng dụng (bao gồm cả kế toán) sử dụng số nguyên. Để đếm số người đi qua cổng, hãy sử dụng int hoặc long.


2
Câu hỏi được gắn thẻ [java]. Định nghĩa ngôn ngữ Java không có quy định nào đối với một vài bit của độ chính xác, chỉ dành cho một số bit lũy thừa bổ sung (và điều đó chỉ khi bạn không sử dụng strictfp). Chỉ vì bạn đã từ bỏ để hiểu điều gì đó không có nghĩa là không thể hiểu được và người khác cũng không nên từ bỏ để hiểu nó. Xem stackoverflow.com/questions/18496560 như một ví dụ về độ dài mà các triển khai Java sẽ sử dụng để triển khai định nghĩa ngôn ngữ (không bao gồm bất kỳ quy định nào cho các bit chính xác bổ sung cũng như strictfp, đối với bất kỳ bit exp nào)
Pascal Cuoq
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.