Toán học dấu phẩy động có bị hỏng không?

Feb 26 2009

Hãy xem xét đoạn mã sau:

0.1 + 0.2 == 0.3  ->  false
0.1 + 0.2         ->  0.30000000000000004

Tại sao những điều không chính xác này lại xảy ra?

Trả lời

2379 DanielScott Feb 26 2009 at 04:40

Phép toán dấu phẩy động nhị phân là như thế này. Trong hầu hết các ngôn ngữ lập trình, nó dựa trên tiêu chuẩn IEEE 754 . Điểm mấu chốt của vấn đề là các số được biểu diễn ở định dạng này dưới dạng một số nguyên nhân với lũy thừa của hai; số hữu tỉ (ví dụ như 0.1, đó là 1/10) có mẫu số là không phải là một sức mạnh của hai không thể được đại diện một cách chính xác.

Đối với định dạng 0.1chuẩn binary64, biểu diễn có thể được viết chính xác như

  • 0.1000000000000000055511151231257827021181583404541015625 ở dạng thập phân, hoặc
  • 0x1.999999999999ap-4trong ký hiệu hexfloat C99 .

Ngược lại, số lượng hợp lý 0.1, đó là 1/10, có thể được viết chính xác như

  • 0.1 ở dạng thập phân, hoặc
  • 0x1.99999999999999...p-4trong một ký hiệu tương tự của ký hiệu hexfloat C99, trong đó ký hiệu này ...đại diện cho một chuỗi 9 không liên tục.

Các hằng số 0.20.3trong chương trình của bạn cũng sẽ là các giá trị gần đúng với giá trị thực của chúng. Nó sẽ xảy ra rằng gần nhất doubleđể 0.2được lớn hơn số hợp lý 0.2nhưng điều đó gần nhất doubleđể 0.3là nhỏ hơn so với số lượng hợp lý 0.3. Tổng 0.10.2kết thúc lớn hơn số hữu tỉ 0.3và do đó không đồng ý với hằng số trong mã của bạn.

Một cách xử lý khá toàn diện về các vấn đề số học dấu phẩy động là Điều Mọi Nhà Khoa Học Máy Tính Nên Biết Về Số Học Dấu Phẩy Động . Để có lời giải thích dễ hiểu hơn, hãy xem float-point-gui.de .

Lưu ý bên: Tất cả các hệ thống số vị trí (cơ số-N) đều có chung vấn đề này với độ chính xác

Các số thập phân cũ thuần túy (cơ số 10) có cùng vấn đề, đó là lý do tại sao các số như 1/3 kết thúc bằng 0,333333333 ...

Bạn vừa vấp phải một số (3/10) dễ biểu diễn với hệ thập phân, nhưng không phù hợp với hệ nhị phân. Nó cũng đi theo cả hai cách (ở một mức độ nhỏ): 1/16 là một số xấu trong hệ thập phân (0,0625), nhưng trong hệ nhị phân, nó trông gọn gàng như một phần 10.000 trong thập phân (0,0001) ** - nếu chúng ta ở thói quen sử dụng hệ thống số cơ số 2 trong cuộc sống hàng ngày của chúng ta, bạn thậm chí nhìn vào con số đó và hiểu theo bản năng rằng bạn có thể đến đó bằng cách giảm một nửa thứ gì đó, giảm một nửa, lặp đi lặp lại.

** Tất nhiên, đó không phải là cách chính xác số dấu phẩy động được lưu trữ trong bộ nhớ (chúng sử dụng một dạng ký hiệu khoa học). Tuy nhiên, nó minh họa quan điểm rằng các lỗi chính xác dấu phẩy động nhị phân có xu hướng tăng lên bởi vì các số "thế giới thực" mà chúng ta thường quan tâm đến việc làm việc với thường là lũy thừa của mười - nhưng chỉ vì chúng ta sử dụng hệ thống số thập phân hàng ngày- hôm nay. Đây cũng là lý do tại sao chúng ta sẽ nói những thứ như 71% thay vì "5 trên 7" (71% là con số gần đúng, vì 5/7 không thể được biểu diễn chính xác bằng bất kỳ số thập phân nào).

Vì vậy, không: số dấu phẩy động nhị phân không bị hỏng, chúng chỉ xảy ra không hoàn hảo như mọi hệ thống số cơ số N khác :)

Lưu ý bên lề: Làm việc với Floats trong lập trình

Trên thực tế, vấn đề về độ chính xác này có nghĩa là bạn cần sử dụng các hàm làm tròn để làm tròn số dấu phẩy động của bạn thành bao nhiêu chữ số thập phân mà bạn quan tâm trước khi hiển thị chúng.

Bạn cũng cần thay thế các bài kiểm tra bình đẳng bằng các phép so sánh cho phép một số dung sai, có nghĩa là:

Đừng không làmif (x == y) { ... }

Thay vào đó hãy làm if (abs(x - y) < myToleranceValue) { ... }.

nơi abslà giá trị tuyệt đối. myToleranceValuecần được chọn cho ứng dụng cụ thể của bạn - và nó sẽ liên quan nhiều đến việc bạn chuẩn bị cho phép bao nhiêu "khoảng trống" và con số lớn nhất mà bạn sẽ so sánh có thể là bao nhiêu (do mất các vấn đề về độ chính xác ). Hãy coi chừng các hằng số kiểu "epsilon" trong ngôn ngữ bạn chọn. Chúng không được sử dụng làm giá trị dung sai.

625 KernelPanik Apr 18 2013 at 18:52

Quan điểm của một nhà thiết kế phần cứng

Tôi tin rằng tôi nên thêm quan điểm của nhà thiết kế phần cứng vào điều này vì tôi thiết kế và xây dựng phần cứng dấu chấm động. Biết được nguồn gốc của lỗi có thể giúp hiểu được những gì đang xảy ra trong phần mềm và cuối cùng, tôi hy vọng điều này sẽ giúp giải thích lý do tại sao lỗi dấu chấm động xảy ra và dường như tích lũy theo thời gian.

1. Tổng quan

Từ góc độ kỹ thuật, hầu hết các hoạt động dấu phẩy động sẽ có một số lỗi do phần cứng thực hiện các phép tính dấu phẩy động chỉ được yêu cầu có lỗi nhỏ hơn một nửa của một đơn vị ở vị trí cuối cùng. Do đó, nhiều phần cứng sẽ dừng lại ở độ chính xác chỉ cần thiết để mang lại sai số dưới một nửa của một đơn vị ở vị trí cuối cùng cho một hoạt động duy nhất , đặc biệt là vấn đề trong phép phân chia dấu phẩy động. Điều gì tạo nên một phép toán đơn lẻ phụ thuộc vào số lượng toán hạng mà đơn vị nhận. Đối với hầu hết, nó là hai, nhưng một số đơn vị có 3 toán hạng trở lên. Do đó, không có gì đảm bảo rằng các thao tác lặp lại sẽ dẫn đến một lỗi mong muốn vì các lỗi sẽ cộng dồn theo thời gian.

2. Tiêu chuẩn

Hầu hết các bộ xử lý tuân theo tiêu chuẩn IEEE-754 nhưng một số sử dụng tiêu chuẩn không chuẩn hóa hoặc các tiêu chuẩn khác. Ví dụ, có một chế độ không chuẩn hóa trong IEEE-754 cho phép biểu diễn các số dấu phẩy động rất nhỏ với chi phí chính xác. Tuy nhiên, phần sau sẽ đề cập đến chế độ chuẩn hóa của IEEE-754 là chế độ hoạt động điển hình.

Trong tiêu chuẩn IEEE-754, các nhà thiết kế phần cứng được phép bất kỳ giá trị nào của lỗi / epsilon miễn là nó nhỏ hơn một nửa của một đơn vị ở vị trí cuối cùng và kết quả chỉ phải nhỏ hơn một nửa của một đơn vị ở vị trí cuối cùng địa điểm cho một hoạt động. Điều này giải thích tại sao khi có các thao tác lặp lại, các lỗi sẽ cộng dồn. Đối với độ chính xác kép IEEE-754, đây là bit thứ 54, vì 53 bit được sử dụng để biểu diễn phần số (chuẩn hóa), còn được gọi là phần định trị, của số dấu phẩy động (ví dụ: 5.3 trong 5.3e5). Các phần tiếp theo đi vào chi tiết hơn về nguyên nhân gây ra lỗi phần cứng trên các hoạt động dấu chấm động khác nhau.

3. Nguyên nhân của lỗi làm tròn trong bộ phận

Nguyên nhân chính của lỗi trong phép chia dấu phẩy động là các thuật toán chia được sử dụng để tính thương số. Hầu hết các hệ thống máy tính tính toán phân chia sử dụng phép nhân bởi một nghịch đảo, chủ yếu ở Z=X/Y, Z = X * (1/Y). Phép chia được tính toán lặp đi lặp lại, tức là mỗi chu kỳ tính toán một số bit của thương số cho đến khi đạt được độ chính xác mong muốn, đối với IEEE-754 là bất kỳ thứ gì có sai số nhỏ hơn một đơn vị ở vị trí cuối cùng. Bảng nghịch đảo của Y (1 / Y) được gọi là bảng chọn thương (QST) trong phép chia chậm và kích thước tính bằng bit của bảng chọn thương thường là chiều rộng của cơ số hoặc một số bit của thương số được tính trong mỗi lần lặp, cộng với một vài bit bảo vệ. Đối với tiêu chuẩn IEEE-754, độ chính xác kép (64-bit), nó sẽ là kích thước của cơ số của bộ chia, cộng với một vài bit bảo vệ k, ở đâu k>=2. Vì vậy, ví dụ, một Bảng lựa chọn thương số điển hình cho một bộ chia tính 2 bit của thương tại một thời điểm (cơ số 4) sẽ là 2+2= 4các bit (cộng với một vài bit tùy chọn).

3.1 Sai số làm tròn bộ phận: Khoảng đối ứng

Những nghịch đảo nào trong bảng lựa chọn thương phụ thuộc vào phương pháp chia : phép chia chậm như phép chia SRT, hoặc phép chia nhanh như phép chia Goldschmidt; mỗi mục nhập được sửa đổi theo thuật toán phân chia nhằm cố gắng mang lại sai số thấp nhất có thể. Tuy nhiên, trong bất kỳ trường hợp nào, tất cả các số tương hỗ đều là các giá trị xấp xỉ của số tương hỗ thực tế và đưa vào một số yếu tố sai số. Cả hai phương pháp chia chậm và chia nhanh đều tính toán thương theo cách lặp đi lặp lại, tức là một số bit của thương được tính từng bước, sau đó kết quả được trừ cho số bị chia và bộ chia lặp lại các bước cho đến khi sai số nhỏ hơn một nửa của một. đơn vị ở vị trí cuối cùng. Phương pháp chia chậm tính toán một số chữ số cố định của thương trong mỗi bước và thường ít tốn kém hơn để xây dựng, và phương pháp chia nhanh tính toán một số chữ số thay đổi trên mỗi bước và thường tốn kém hơn để xây dựng. Phần quan trọng nhất của các phương pháp chia là hầu hết chúng dựa vào phép nhân lặp đi lặp lại với một xấp xỉ của một nghịch đảo, vì vậy chúng dễ bị sai.

4. Các lỗi làm tròn trong các hoạt động khác: Cắt ngắn

Một nguyên nhân khác gây ra lỗi làm tròn trong tất cả các thao tác là các chế độ cắt ngắn khác nhau của câu trả lời cuối cùng mà IEEE-754 cho phép. Có cắt ngắn, làm tròn về phía không, làm tròn đến gần nhất (mặc định), làm tròn xuống và làm tròn lên. Tất cả các phương pháp giới thiệu một phần tử có sai số nhỏ hơn một đơn vị ở vị trí cuối cùng cho một hoạt động duy nhất. Theo thời gian và các hoạt động lặp đi lặp lại, việc cắt bớt cũng cộng dồn vào lỗi kết quả. Lỗi cắt ngắn này đặc biệt có vấn đề trong phép tính lũy thừa, liên quan đến một số dạng nhân lặp lại.

5. Hoạt động lặp lại

Vì phần cứng thực hiện các phép tính dấu phẩy động chỉ cần mang lại kết quả với sai số nhỏ hơn một nửa đơn vị ở vị trí cuối cùng cho một thao tác duy nhất, nên lỗi sẽ phát triển qua các thao tác lặp lại nếu không được theo dõi. Đây là lý do mà trong các phép tính yêu cầu lỗi giới hạn, các nhà toán học sử dụng các phương pháp như sử dụng chữ số chẵn làm tròn đến gần nhất ở vị trí cuối cùng của IEEE-754, bởi vì, theo thời gian, các lỗi có nhiều khả năng triệt tiêu lẫn nhau. out, và Interval Arithmetic kết hợp với các biến thể của chế độ làm tròn IEEE 754 để dự đoán các lỗi làm tròn và sửa chúng. Do sai số tương đối thấp so với các chế độ làm tròn khác, làm tròn đến chữ số chẵn gần nhất (ở vị trí cuối cùng), là chế độ làm tròn mặc định của IEEE-754.

Lưu ý rằng chế độ làm tròn mặc định, làm tròn đến chữ số chẵn gần nhất ở vị trí cuối cùng , đảm bảo sai số nhỏ hơn một nửa của một đơn vị ở vị trí cuối cùng cho một thao tác. Chỉ sử dụng tính năng cắt bớt, làm tròn lên và làm tròn xuống một mình có thể dẫn đến lỗi lớn hơn một nửa đơn vị ở vị trí cuối cùng, nhưng ít hơn một đơn vị ở vị trí cuối cùng, do đó, các chế độ này không được khuyến nghị trừ khi chúng được sử dụng trong Số học khoảng thời gian.

6. Tóm tắt

Nói tóm lại, lý do cơ bản gây ra các lỗi trong các phép toán dấu phẩy động là sự kết hợp giữa việc cắt bớt phần cứng và sự cắt bớt một phần đối ứng trong trường hợp phân chia. Vì tiêu chuẩn IEEE-754 chỉ yêu cầu sai số dưới một nửa của một đơn vị ở vị trí cuối cùng cho một thao tác duy nhất, nên các lỗi dấu phẩy động qua các thao tác lặp lại sẽ cộng lại trừ khi được sửa chữa.

481 JoelCoehoorn Feb 26 2009 at 04:43

Nó bị hỏng theo cùng một cách ký hiệu thập phân (cơ số 10) bị hỏng, chỉ dành cho cơ số 2.

Để hiểu, hãy nghĩ về việc biểu diễn 1/3 dưới dạng giá trị thập phân. Không thể làm chính xác được! Theo cách tương tự, 1/10 (0,1 thập phân) không thể được biểu diễn chính xác trong cơ số 2 (nhị phân) dưới dạng giá trị "thập phân"; một mô hình lặp lại sau dấu thập phân tiếp diễn mãi mãi. Giá trị không chính xác và do đó bạn không thể thực hiện phép toán chính xác với nó bằng các phương pháp dấu phẩy động thông thường.

322 ChrisJester-Young Nov 20 2014 at 09:39

Hầu hết các câu trả lời ở đây giải quyết câu hỏi này bằng các thuật ngữ kỹ thuật, rất khô khan. Tôi muốn giải quyết vấn đề này theo những thuật ngữ mà con người bình thường có thể hiểu được.

Hãy tưởng tượng rằng bạn đang cố gắng cắt miếng pizza. Bạn có một máy cắt bánh pizza bằng rô-bốt có thể cắt chính xác một nửa các lát bánh pizza . Nó có thể giảm một nửa toàn bộ chiếc bánh pizza hoặc nó có thể giảm một nửa một miếng bánh hiện có, nhưng trong mọi trường hợp, việc giảm một nửa luôn chính xác.

Máy cắt bánh pizza đó có chuyển động rất tốt và nếu bạn bắt đầu với một chiếc bánh pizza nguyên chiếc, sau đó giảm một nửa và tiếp tục cắt một nửa lát nhỏ nhất mỗi lần, bạn có thể thực hiện cắt một nửa 53 lần trước khi lát quá nhỏ so với khả năng chính xác cao của nó . Tại thời điểm đó, bạn không thể giảm một nửa lát rất mỏng đó nữa, mà phải bao gồm hoặc loại trừ nó như hiện tại.

Bây giờ, làm thế nào bạn có thể cắt tất cả các lát theo cách mà có thể tạo ra một phần mười (0,1) hoặc một phần năm (0,2) của một chiếc bánh pizza? Hãy thực sự suy nghĩ về nó và thử tìm hiểu nó. Bạn thậm chí có thể thử sử dụng một chiếc bánh pizza thực sự, nếu bạn có một chiếc máy cắt bánh pizza chính xác thần thoại trong tay. :-)


Tất nhiên, hầu hết các lập trình viên có kinh nghiệm đều biết câu trả lời thực sự, đó là không có cách nào để ghép chính xác một phần mười hoặc phần năm chiếc bánh pizza bằng cách sử dụng những lát đó, bất kể bạn cắt chúng có mịn như thế nào. Bạn có thể thực hiện một phép tính gần đúng khá tốt và nếu bạn cộng giá trị xấp xỉ 0,1 với giá trị xấp xỉ 0,2, bạn sẽ nhận được giá trị gần đúng khá tốt là 0,3, nhưng nó vẫn chỉ là một ước tính gần đúng.

Đối với các số có độ chính xác kép (là độ chính xác cho phép bạn giảm một nửa chiếc bánh pizza của mình 53 lần), các số ngay lập tức nhỏ hơn và lớn hơn 0,1 là 0,09999999999999999167332731531132594682276248931884765625 và 0,10000000000000055511151231257827021181583404541015625. Cái sau gần hơn 0,1 một chút so với cái trước, vì vậy trình phân tích cú pháp số, với đầu vào là 0,1, sẽ ưu tiên cái sau.

(Sự khác biệt giữa hai con số đó là "lát nhỏ nhất" mà chúng ta phải quyết định đưa vào, đưa vào xu hướng tăng hoặc loại trừ, tạo ra xu hướng giảm. Thuật ngữ kỹ thuật cho lát nhỏ nhất đó là ulp .)

Trong trường hợp 0,2, các số đều giống nhau, chỉ được nhân lên theo hệ số 2. Một lần nữa, chúng tôi ưu tiên giá trị cao hơn 0,2 một chút.

Lưu ý rằng trong cả hai trường hợp, các giá trị xấp xỉ 0,1 và 0,2 có độ chệch hướng lên một chút. Nếu chúng ta thêm đủ các thành phần này vào, chúng sẽ đẩy con số ngày càng xa so với những gì chúng ta muốn và trên thực tế, trong trường hợp 0,1 + 0,2, độ lệch đủ cao để con số kết quả không còn là con số gần nhất đến 0,3.

Cụ thể, 0,1 + 0,2 thực sự là 0,1000000000000000055511151231257827021181583404541015625 + 0,2000000000000011102230246251565404236316680908203125 = 0,30000000000000004440892098500626991699699938937689389759389759 thực tế là


PS Một số ngôn ngữ lập trình cũng cung cấp máy cắt bánh pizza có thể chia các lát cắt thành phần mười chính xác . Mặc dù những chiếc máy cắt bánh pizza như vậy không phổ biến, nhưng nếu bạn có quyền sử dụng, bạn nên sử dụng nó khi điều quan trọng là có thể cắt được chính xác 1/10 hoặc 1/5 lát.

(Ban đầu được đăng trên Quora.)

215 DevinJeanpierre Feb 26 2009 at 04:41

Lỗi làm tròn dấu chấm động. 0,1 không thể được biểu diễn chính xác trong cơ số 2 như trong cơ số 10 do thiếu thừa số nguyên tố là 5. Cũng như 1/3 lấy vô số chữ số để biểu diễn dưới dạng thập phân, nhưng là "0,1" trong cơ số 3, 0,1 nhận vô số chữ số trong cơ số 2, trong đó nó không có trong cơ số 10. Và máy tính không có bộ nhớ vô hạn.

126 DanielVassallo Apr 09 2010 at 19:25

Ngoài các câu trả lời đúng khác, bạn có thể muốn xem xét việc chia tỷ lệ các giá trị của mình để tránh các vấn đề với số học dấu phẩy động.

Ví dụ:

var result = 1.0 + 2.0;     // result === 3.0 returns true

... thay vì:

var result = 0.1 + 0.2;     // result === 0.3 returns false

Biểu thức 0.1 + 0.2 === 0.3trả về falsetrong JavaScript, nhưng may mắn thay, số học số nguyên trong dấu phẩy động là chính xác, vì vậy có thể tránh được lỗi biểu diễn thập phân bằng cách chia tỷ lệ.

Như một ví dụ thực tế, để tránh các vấn đề nổi-điểm mà chính xác là tối quan trọng, nó được khuyến khích 1 để xử lý tiền như một số nguyên đại diện cho số cent: 2550xu thay vì 25.50USD.


1 Douglas Crockford: JavaScript: Những phần hay : Phụ lục A - Những phần đáng kinh ngạc (trang 105) .

120 WaiHaLee Feb 24 2015 at 00:15

Câu trả lời của tôi khá dài, vì vậy tôi đã chia nó thành ba phần. Vì câu hỏi là về toán học dấu phẩy động, tôi đã nhấn mạnh vào những gì máy thực sự làm. Tôi cũng đã làm cho nó cụ thể với độ chính xác gấp đôi (64 bit), nhưng đối số áp dụng như nhau cho bất kỳ số học dấu phẩy động nào.

Mở đầu

Một IEEE 754 đôi chính xác nhị phân định dạng dấu chấm động (binary64) số đại diện cho một số hình thức

giá trị = (-1) ^ s * (1.m 51 m 50 ... m 2 m 1 m 0 ) 2 * 2 e-1023

trong 64 bit:

  • Bit đầu tiên là bit dấu : 1nếu là số âm, 0ngược lại là 1 .
  • 11 bit tiếp theo là số mũ , được bù bởi 1023. Nói cách khác, sau khi đọc các bit lũy thừa từ một số có độ chính xác kép, 1023 phải được trừ đi để có được lũy thừa của hai.
  • 52 bit còn lại là phần nghĩa và phần định trị. Trong phần định trị, một 'ngụ ý' 1.luôn bị bỏ qua 2 vì bit quan trọng nhất của bất kỳ giá trị nhị phân nào là 1.

1 - IEEE 754 cho phép khái niệm số 0 có dấu - +0-0được xử lý theo cách khác: 1 / (+0)là dương vô cực; 1 / (-0)là âm vô cùng. Đối với các giá trị bằng không, các bit định trị và số mũ đều bằng không. Lưu ý: các giá trị 0 (+0 và -0) rõ ràng không được phân loại là 2 không bình thường .

2 - Đây không phải là trường hợp của các số không bình thường , có số mũ bù bằng 0 (và hàm ý 0.). Phạm vi của số chính xác kép bất thường là d min ≤ | x | ≤ d max , trong đó d min (số khác không đại diện nhỏ nhất) là 2 -1023 - 51 (≈ 4,94 * 10 -324 ) và d max (số bất thường lớn nhất, mà phần định trị bao gồm hoàn toàn bằng 1s) là 2-1023 + 1 - 2 -1023 - 51 (≈ 2.225 * 10 -308 ).


Chuyển một số chính xác kép thành nhị phân

Nhiều trình chuyển đổi trực tuyến tồn tại để chuyển đổi số dấu phẩy động có độ chính xác kép thành số nhị phân (ví dụ: tại binaryconvert.com ), nhưng đây là một số mã C # mẫu để có được biểu diễn IEEE 754 cho số chính xác kép (tôi phân tách ba phần bằng dấu hai chấm ( :) :

public static string BinaryRepresentation(double value)
{
    long valueInLongType = BitConverter.DoubleToInt64Bits(value);
    string bits = Convert.ToString(valueInLongType, 2);
    string leadingZeros = new string('0', 64 - bits.Length);
    string binaryRepresentation = leadingZeros + bits;

    string sign = binaryRepresentation[0].ToString();
    string exponent = binaryRepresentation.Substring(1, 11);
    string mantissa = binaryRepresentation.Substring(12);

    return string.Format("{0}:{1}:{2}", sign, exponent, mantissa);
}

Đi vào vấn đề: câu hỏi ban đầu

(Bỏ qua phần cuối đối với phiên bản TL; DR)

Cato Johnston (người đặt câu hỏi) hỏi tại sao 0,1 + 0,2! = 0,3.

Được viết dưới dạng nhị phân (với dấu hai chấm phân cách ba phần), các biểu diễn IEEE 754 của các giá trị là:

0.1 => 0:01111111011:1001100110011001100110011001100110011001100110011010
0.2 => 0:01111111100:1001100110011001100110011001100110011001100110011010

Lưu ý rằng phần định trị bao gồm các chữ số lặp lại của 0011. Đây là chìa khóa cho lý do tại sao có bất kỳ lỗi nào đối với các phép tính - 0,1, 0,2 và 0,3 không thể được biểu diễn chính xác trong hệ nhị phân trong một số lượng hữu hạn các bit nhị phân bất kỳ nhiều hơn 1/9, 1/3 hoặc 1/7 có thể được biểu diễn chính xác trong chữ số thập phân .

Cũng lưu ý rằng chúng ta có thể giảm lũy thừa trong số mũ đi 52 và dịch chuyển điểm trong biểu diễn nhị phân sang phải 52 vị trí (giống như 10 -3 * 1.23 == 10 -5 * 123). Sau đó, điều này cho phép chúng tôi biểu diễn biểu diễn nhị phân dưới dạng giá trị chính xác mà nó biểu diễn ở dạng a * 2 p . trong đó 'a' là một số nguyên.

Chuyển đổi số mũ thành số thập phân, loại bỏ phần bù và cộng lại hàm ý 1(trong dấu ngoặc vuông), 0,1 và 0,2 là:

0.1 => 2^-4 * [1].1001100110011001100110011001100110011001100110011010
0.2 => 2^-3 * [1].1001100110011001100110011001100110011001100110011010
or
0.1 => 2^-56 * 7205759403792794 = 0.1000000000000000055511151231257827021181583404541015625
0.2 => 2^-55 * 7205759403792794 = 0.200000000000000011102230246251565404236316680908203125

Để cộng hai số, số mũ cần phải giống nhau, tức là:

0.1 => 2^-3 *  0.1100110011001100110011001100110011001100110011001101(0)
0.2 => 2^-3 *  1.1001100110011001100110011001100110011001100110011010
sum =  2^-3 * 10.0110011001100110011001100110011001100110011001100111
or
0.1 => 2^-55 * 3602879701896397  = 0.1000000000000000055511151231257827021181583404541015625
0.2 => 2^-55 * 7205759403792794  = 0.200000000000000011102230246251565404236316680908203125
sum =  2^-55 * 10808639105689191 = 0.3000000000000000166533453693773481063544750213623046875

Vì tổng không có dạng 2 n * 1. {bbb}, chúng tôi tăng số mũ lên một và dịch chuyển điểm thập phân ( nhị phân ) để nhận được:

sum = 2^-2  * 1.0011001100110011001100110011001100110011001100110011(1)
    = 2^-54 * 5404319552844595.5 = 0.3000000000000000166533453693773481063544750213623046875

Bây giờ có 53 bit trong phần định trị (phần thứ 53 nằm trong dấu ngoặc vuông ở dòng trên). Chế độ làm tròn mặc định cho IEEE 754 là 'Làm tròn đến gần nhất ' - tức là nếu một số x nằm giữa hai giá trị ab , thì giá trị mà bit có ý nghĩa nhỏ nhất bằng 0 sẽ được chọn.

a = 2^-54 * 5404319552844595 = 0.299999999999999988897769753748434595763683319091796875
  = 2^-2  * 1.0011001100110011001100110011001100110011001100110011

x = 2^-2  * 1.0011001100110011001100110011001100110011001100110011(1)

b = 2^-2  * 1.0011001100110011001100110011001100110011001100110100
  = 2^-54 * 5404319552844596 = 0.3000000000000000444089209850062616169452667236328125

Lưu ý rằng ab chỉ khác nhau ở bit cuối cùng; ...0011+ 1= ...0100. Trong trường hợp này, giá trị có bit có ý nghĩa nhỏ nhất bằng 0 là b , vì vậy tổng là:

sum = 2^-2  * 1.0011001100110011001100110011001100110011001100110100
    = 2^-54 * 5404319552844596 = 0.3000000000000000444089209850062616169452667236328125

trong khi biểu diễn nhị phân của 0,3 là:

0.3 => 2^-2  * 1.0011001100110011001100110011001100110011001100110011
    =  2^-54 * 5404319552844595 = 0.299999999999999988897769753748434595763683319091796875

chỉ khác với biểu diễn nhị phân của tổng 0,1 và 0,2 x 2 -54 .

Biểu diễn nhị phân 0,1 và 0,2 là các đại diện chính xác nhất của các số được IEEE 754. Việc bổ sung các biểu diễn này, do chế độ làm tròn mặc định, dẫn đến một giá trị chỉ khác ở bit có nghĩa nhỏ nhất.

TL; DR

Viết 0.1 + 0.2bằng biểu diễn nhị phân IEEE 754 (với dấu hai chấm phân tách ba phần) và so sánh với nó 0.3, đây là (Tôi đã đặt các bit riêng biệt trong dấu ngoặc vuông):

0.1 + 0.2 => 0:01111111101:0011001100110011001100110011001100110011001100110[100]
0.3       => 0:01111111101:0011001100110011001100110011001100110011001100110[011]

Được chuyển đổi trở lại thành số thập phân, các giá trị này là:

0.1 + 0.2 => 0.300000000000000044408920985006...
0.3       => 0.299999999999999988897769753748...

Sự khác biệt chính xác là 2 -54 , là ~ 5.5511151231258 × 10 -17 - không đáng kể (đối với nhiều ứng dụng) khi so sánh với các giá trị ban đầu.

So sánh các bit cuối cùng của một số dấu phẩy động vốn dĩ rất nguy hiểm, như bất kỳ ai đọc cuốn sách nổi tiếng " Mọi nhà khoa học máy tính nên biết về số học dấu phẩy động " (bao gồm tất cả các phần chính của câu trả lời này) sẽ biết.

Hầu hết các máy tính sử dụng các chữ số bảo vệ bổ sung để giải quyết vấn đề này, đó là cách 0.1 + 0.2sẽ cung cấp 0.3: một số bit cuối cùng được làm tròn.

59 MarkRansom Mar 16 2016 at 12:27

Số dấu phẩy động được lưu trữ trong máy tính bao gồm hai phần, một số nguyên và một số mũ mà cơ số được lấy và nhân với phần nguyên.

Nếu máy tính đang hoạt động ở cơ sở 10, 0.1sẽ 1 x 10⁻¹, 0.2sẽ 2 x 10⁻¹0.3sẽ như vậy 3 x 10⁻¹. Phép toán số nguyên rất dễ dàng và chính xác, vì vậy việc thêm vào 0.1 + 0.2hiển nhiên sẽ dẫn đến kết quả 0.3.

Máy tính thường không hoạt động ở cơ sở 10, chúng hoạt động ở cơ sở 2. Bạn vẫn có thể nhận được kết quả chính xác cho một số giá trị, ví dụ như 0.51 x 2⁻¹0.25đang có 1 x 2⁻², và thêm chúng vào kết quả là 3 x 2⁻², hoặc 0.75. Chính xác.

Vấn đề xảy ra với những con số có thể được biểu diễn chính xác trong cơ số 10, nhưng không được biểu diễn trong cơ số 2. Những con số đó cần được làm tròn đến mức tương đương gần nhất của chúng. Giả sử định dạng dấu chấm động 64-bit IEEE rất phổ biến, số gần nhất 0.13602879701896397 x 2⁻⁵⁵và số gần nhất với 0.27205759403792794 x 2⁻⁵⁵; cộng chúng lại với nhau dẫn đến 10808639105689191 x 2⁻⁵⁵hoặc một giá trị thập phân chính xác là 0.3000000000000000444089209850062616169452667236328125. Số dấu phẩy động thường được làm tròn để hiển thị.

49 BrettDaniel Feb 26 2009 at 04:42

Lỗi làm tròn dấu chấm động. Từ những điều mà mọi nhà khoa học máy tính nên biết về số học dấu chấm động :

Việc nén vô hạn số thực thành một số bit hữu hạn yêu cầu một biểu diễn gần đúng. Mặc dù có vô số số nguyên, trong hầu hết các chương trình, kết quả của các phép tính số nguyên có thể được lưu trữ trong 32 bit. Ngược lại, với bất kỳ số bit cố định nào, hầu hết các phép tính với số thực sẽ tạo ra các đại lượng không thể được biểu diễn chính xác bằng cách sử dụng nhiều bit đó. Do đó, kết quả của phép tính dấu phẩy động thường phải được làm tròn để phù hợp với biểu diễn hữu hạn của nó. Lỗi làm tròn này là tính năng đặc trưng của phép tính dấu phẩy động.

33 Justineo Dec 26 2011 at 13:51

Cách giải quyết của tôi:

function add(a, b, precision) {
    var x = Math.pow(10, precision || 2);
    return (Math.round(a * x) + Math.round(b * x)) / x;
}

độ chính xác đề cập đến số chữ số bạn muốn giữ lại sau dấu thập phân trong khi cộng.

30 bruziuz Oct 06 2014 at 01:39

Rất nhiều câu trả lời hay đã được đăng, nhưng tôi muốn thêm một câu trả lời nữa.

Không phải tất cả các số đều có thể được biểu diễn qua dấu chấm động / số đôi Ví dụ: số "0,2" sẽ được biểu thị là "0,200000003" ở độ chính xác đơn trong tiêu chuẩn dấu chấm động IEEE754.

Mô hình lưu trữ số thực bên dưới biểu thị số thực như

Mặc dù bạn có thể gõ 0.2dễ dàng, FLT_RADIXDBL_RADIXlà 2; không phải 10 cho máy tính có FPU sử dụng "Tiêu chuẩn IEEE cho số học dấu chấm động nhị phân (ISO / IEEE Std 754-1985)".

Vì vậy, nó là một chút khó khăn để biểu diễn những con số như vậy một cách chính xác. Ngay cả khi bạn chỉ định biến này một cách rõ ràng mà không cần bất kỳ phép tính trung gian nào.

29 KostasChalkias Jan 03 2015 at 19:12

Một số thống kê liên quan đến câu hỏi chính xác kép nổi tiếng này.

Khi thêm tất cả các giá trị ( a + b ) bằng bước 0,1 (từ 0,1 đến 100), chúng ta có ~ 15% khả năng mắc lỗi chính xác . Lưu ý rằng lỗi có thể dẫn đến các giá trị lớn hơn hoặc nhỏ hơn một chút. Dưới đây là một số ví dụ:

0.1 + 0.2 = 0.30000000000000004 (BIGGER)
0.1 + 0.7 = 0.7999999999999999 (SMALLER)
...
1.7 + 1.9 = 3.5999999999999996 (SMALLER)
1.7 + 2.2 = 3.9000000000000004 (BIGGER)
...
3.2 + 3.6 = 6.800000000000001 (BIGGER)
3.2 + 4.4 = 7.6000000000000005 (BIGGER)

Khi trừ tất cả các giá trị ( a - b trong đó a> b ) bằng cách sử dụng bước 0,1 (từ 100 đến 0,1), chúng ta có ~ 34% khả năng sai số chính xác . Dưới đây là một số ví dụ:

0.6 - 0.2 = 0.39999999999999997 (SMALLER)
0.5 - 0.4 = 0.09999999999999998 (SMALLER)
...
2.1 - 0.2 = 1.9000000000000001 (BIGGER)
2.0 - 1.9 = 0.10000000000000009 (BIGGER)
...
100 - 99.9 = 0.09999999999999432 (SMALLER)
100 - 99.8 = 0.20000000000000284 (BIGGER)

* 15% và 34% thực sự là rất lớn, vì vậy hãy luôn sử dụng BigDecimal khi độ chính xác có tầm quan trọng lớn. Với 2 chữ số thập phân (bước 0,01), tình hình tồi tệ hơn một chút (18% và 36%).

28 DigitalRoss Feb 03 2016 at 06:49

Không, không bị hỏng, nhưng hầu hết các phân số thập phân phải gần đúng

Tóm lược

Số học dấu phẩy động chính xác, thật không may, nó không khớp với cách biểu diễn số cơ số 10 thông thường của chúng ta, vì vậy hóa ra chúng ta thường cung cấp đầu vào hơi sai so với những gì chúng ta đã viết.

Ngay cả những số đơn giản như 0,01, 0,02, 0,03, 0,04 ... 0,24 cũng không thể biểu diễn chính xác như phân số nhị phân. Nếu bạn đếm lên 0,01, 0,02, 0,03 ..., không phải cho đến khi bạn đến 0,25, bạn sẽ nhận được phân số đầu tiên có thể biểu diễn trong cơ số 2 . Nếu bạn đã thử sử dụng FP, thì 0,01 của bạn sẽ hơi sai, vì vậy cách duy nhất để thêm 25 trong số chúng lên đến 0,25 chính xác đẹp sẽ yêu cầu một chuỗi nhân quả dài liên quan đến các bit bảo vệ và làm tròn. Thật khó để dự đoán vì vậy chúng tôi ném tay lên và nói rằng "FP là không chính xác", nhưng điều đó không thực sự đúng.

Chúng tôi liên tục cung cấp cho phần cứng FP một cái gì đó có vẻ đơn giản trong cơ sở 10 nhưng lại là một phân số lặp lại trong cơ sở 2.

Làm sao chuyện này lại xảy ra?

Khi chúng ta viết dưới dạng số thập phân, mọi phân số (cụ thể là mọi số thập phân có tận cùng) là một số hữu tỉ có dạng

           a / (2 n x 5 m )

Trong hệ nhị phân, chúng ta chỉ nhận được số hạng thứ 2 n , đó là:

           a / 2 n

Vì vậy, trong thập phân, chúng ta không thể đại diện cho 1 / 3 . Bởi vì cơ số 10 bao gồm 2 như một thừa số nguyên tố, mọi số chúng ta có thể viết dưới dạng phân số nhị phân cũng có thể được viết dưới dạng phân số cơ số 10. Tuy nhiên, hầu như bất cứ thứ gì chúng ta viết dưới dạng phân số cơ số 10 đều có thể biểu diễn được trong hệ nhị phân. Trong phạm vi từ 0,01, 0,02, 0,03 ... 0,99, chỉ có ba số có thể được biểu diễn ở định dạng FP của chúng tôi: 0,25, 0,50 và 0,75, vì chúng là 1/4, 1/2 và 3/4, tất cả các số với một thừa số nguyên tố chỉ sử dụng số hạng thứ 2 n .

Trong cơ sở 10 chúng ta không thể đại diện cho 1 / 3 . Nhưng trong hệ nhị phân, chúng tôi không thể làm được 1 / 10 hay 1 / 3 .

Vì vậy, trong khi mọi phân số nhị phân có thể được viết dưới dạng thập phân, điều ngược lại là không đúng. Và trên thực tế, hầu hết các phân số thập phân đều lặp lại ở dạng nhị phân.

Đối phó với nó

Các nhà phát triển thường được hướng dẫn thực hiện <epsilon so sánh, lời khuyên tốt hơn có thể là làm tròn các giá trị tích phân (trong thư viện C: round () và roundf (), tức là ở định dạng FP) và sau đó so sánh. Làm tròn đến một độ dài phân số thập phân cụ thể giải quyết hầu hết các vấn đề với đầu ra.

Ngoài ra, đối với các bài toán xử lý số thực (các bài toán mà FP được phát minh trên các máy tính đời đầu, đắt tiền), các hằng số vật lý của vũ trụ và tất cả các phép đo khác chỉ được biết đến với một số lượng tương đối nhỏ các số liệu quan trọng, vì vậy toàn bộ không gian bài toán là "không chính xác" dù sao. "Độ chính xác" của FP không phải là vấn đề trong loại ứng dụng này.

Toàn bộ vấn đề thực sự nảy sinh khi mọi người cố gắng sử dụng FP để đếm đậu. Nó hoạt động cho điều đó, nhưng chỉ khi bạn bám vào các giá trị tích phân, loại nào sẽ đánh bại quan điểm của việc sử dụng nó. Đây là lý do tại sao chúng tôi có tất cả các thư viện phần mềm phân số thập phân.

Tôi thích câu trả lời Pizza của Chris , bởi vì nó mô tả vấn đề thực tế, không chỉ là sự truyền tay thông thường về "sự không chính xác". Nếu FP chỉ đơn giản là "không chính xác", chúng tôi có thể khắc phục điều đó và có thể đã làm điều đó nhiều thập kỷ trước. Lý do chúng tôi không làm là vì định dạng FP nhỏ gọn và nhanh chóng và đó là cách tốt nhất để thu thập nhiều số. Ngoài ra, đó là di sản từ thời đại không gian và chạy đua vũ trang và những nỗ lực ban đầu để giải quyết các vấn đề lớn với các máy tính rất chậm sử dụng hệ thống bộ nhớ nhỏ. (Đôi khi, các lõi từ tính riêng lẻ để lưu trữ 1 bit, nhưng đó là một câu chuyện khác. )

Phần kết luận

Nếu bạn chỉ đếm đậu tại ngân hàng, các giải pháp phần mềm sử dụng biểu diễn chuỗi thập phân ngay từ đầu hoạt động hoàn toàn tốt. Nhưng bạn không thể thực hiện sắc động lực học lượng tử hoặc khí động học theo cách đó.

26 MuhammadMusavi Aug 07 2018 at 16:34

Tóm lại là vì:

Số dấu phẩy động không thể đại diện chính xác cho tất cả các số thập phân trong hệ nhị phân

Vì vậy, giống như 3/10 không tồn tại chính xác trong cơ số 10 (nó sẽ là 3,33 ... lặp lại), theo cách tương tự 1/10 không tồn tại trong hệ nhị phân.

Vậy thì sao? Làm thế nào để đối phó với nó? Có bất kỳ công việc xung quanh?

Để đưa ra giải pháp tốt nhất mà tôi có thể nói là tôi đã khám phá ra phương pháp sau:

parseFloat((0.1 + 0.2).toFixed(10)) => Will return 0.3

Hãy để tôi giải thích tại sao đó là giải pháp tốt nhất. Như những người khác đã đề cập trong các câu trả lời ở trên, bạn nên sử dụng hàm ready để sử dụng Javascript toFixed () để giải quyết vấn đề. Nhưng rất có thể bạn sẽ gặp phải một số vấn đề.

Hãy tưởng tượng bạn đang đi để thêm lên hai con số float như 0.20.7ở đây nó là: 0.2 + 0.7 = 0.8999999999999999.

Kết quả mong đợi của bạn là 0.9nó có nghĩa là bạn cần một kết quả có độ chính xác 1 chữ số trong trường hợp này. Vì vậy, bạn nên sử dụng (0.2 + 0.7).tofixed(1)nhưng bạn không thể chỉ cung cấp một tham số nhất định cho toFixed () vì nó phụ thuộc vào số đã cho, chẳng hạn

0.22 + 0.7 = 0.9199999999999999

Trong ví dụ này, bạn cần độ chính xác 2 chữ số toFixed(2), vậy tham số phải là bao nhiêu để phù hợp với mọi số thực đã cho?

Bạn có thể nói hãy để nó là 10 trong mọi tình huống:

(0.2 + 0.7).toFixed(10) => Result will be "0.9000000000"

Chỉ trích! Bạn sẽ làm gì với những số 0 không mong muốn đó sau 9? Đã đến lúc chuyển đổi nó thành float để biến nó thành như bạn mong muốn:

parseFloat((0.2 + 0.7).toFixed(10)) => Result will be 0.9

Bây giờ bạn đã tìm thấy giải pháp, tốt hơn nên cung cấp nó dưới dạng một chức năng như sau:

function floatify(number){
           return parseFloat((number).toFixed(10));
        }
    

Hãy tự mình thử:

function floatify(number){
       return parseFloat((number).toFixed(10));
    }
 
function addUp(){
  var number1 = +$("#number1").val(); var number2 = +$("#number2").val();
  var unexpectedResult = number1 + number2;
  var expectedResult = floatify(number1 + number2);
  $("#unexpectedResult").text(unexpectedResult); $("#expectedResult").text(expectedResult);
}
addUp();
input{
  width: 50px;
}
#expectedResult{
color: green;
}
#unexpectedResult{
color: red;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<input id="number1" value="0.2" onclick="addUp()" onkeyup="addUp()"/> +
<input id="number2" value="0.7" onclick="addUp()" onkeyup="addUp()"/> =
<p>Expected Result: <span id="expectedResult"></span></p>
<p>Unexpected Result: <span id="unexpectedResult"></span></p>

Bạn có thể sử dụng nó theo cách này:

var x = 0.2 + 0.7;
floatify(x);  => Result: 0.9

Như W3SCHOOLS gợi ý cũng có một giải pháp khác, bạn có thể nhân và chia để giải quyết vấn đề trên:

var x = (0.2 * 10 + 0.1 * 10) / 10;       // x will be 0.3

Hãy nhớ rằng điều đó (0.2 + 0.1) * 10 / 10sẽ không hoạt động mặc dù nó có vẻ giống nhau! Tôi thích giải pháp đầu tiên hơn vì tôi có thể áp dụng nó như một hàm chuyển đổi float đầu vào thành float đầu ra chính xác.

18 workoverflow Aug 01 2012 at 14:02

Bạn đã thử giải pháp băng keo chưa?

Cố gắng xác định thời điểm xảy ra lỗi và sửa chúng bằng câu lệnh if ngắn, nó không đẹp nhưng đối với một số vấn đề, đây là giải pháp duy nhất và đây là một trong số đó.

 if( (n * 0.1) < 100.0 ) { return n * 0.1 - 0.000000000000001 ;}
                    else { return n * 0.1 + 0.000000000000001 ;}    

Tôi đã gặp vấn đề tương tự trong một dự án mô phỏng khoa học trong c # và tôi có thể nói với bạn rằng nếu bạn bỏ qua hiệu ứng con bướm, nó sẽ biến thành một con rồng to béo và cắn bạn trong **

16 PiyushS528 Oct 14 2013 at 23:45

Những con số kỳ lạ đó xuất hiện do máy tính sử dụng hệ thống số nhị phân (cơ số 2) cho mục đích tính toán, trong khi chúng ta sử dụng hệ thập phân (cơ số 10).

Có phần lớn các số dạng phân số không thể được biểu diễn chính xác ở dạng nhị phân hoặc thập phân hoặc cả hai. Kết quả - Kết quả số được làm tròn (nhưng chính xác).

16 AndreaCorbellini Aug 21 2015 at 21:53

Cho rằng chưa ai đề cập đến điều này ...

Một số ngôn ngữ cấp cao như Python và Java đi kèm với các công cụ để khắc phục các hạn chế về dấu chấm động nhị phân. Ví dụ:

  • decimalMô-đun của Python và BigDecimallớp của Java , đại diện cho các số bên trong với ký hiệu thập phân (trái ngược với ký hiệu nhị phân). Cả hai đều có độ chính xác hạn chế, vì vậy chúng vẫn dễ bị lỗi, tuy nhiên chúng giải quyết được hầu hết các vấn đề phổ biến với số học dấu phẩy động nhị phân.

    Số thập phân rất tốt khi giao dịch với tiền: mười xu cộng với hai mươi xu luôn chính xác là ba mươi xu:

    >>> 0.1 + 0.2 == 0.3
    False
    >>> Decimal('0.1') + Decimal('0.2') == Decimal('0.3')
    True
    

    decimalMô-đun của Python dựa trên tiêu chuẩn IEEE 854-1987 .

  • fractionsMô-đun của Python và BigFractionlớp của Apache Common . Cả hai đều đại diện cho các số hữu tỉ dưới dạng (numerator, denominator)các cặp và chúng có thể cho kết quả chính xác hơn số học dấu phẩy động thập phân.

Không có giải pháp nào trong số này là hoàn hảo (đặc biệt là nếu chúng ta xem xét biểu diễn hoặc nếu chúng ta yêu cầu độ chính xác rất cao), nhưng chúng vẫn giải quyết được rất nhiều vấn đề với số học dấu phẩy động nhị phân.

16 PatriciaShanahan Dec 21 2015 at 18:15

Nhiều bản sao của câu hỏi này hỏi về tác động của việc làm tròn dấu phẩy động đối với các số cụ thể. Trong thực tế, sẽ dễ dàng hiểu được cách thức hoạt động của nó bằng cách xem kết quả chính xác của các phép tính quan tâm hơn là chỉ đọc về nó. Một số ngôn ngữ cung cấp các cách thực hiện điều đó - chẳng hạn như chuyển đổi một floathoặc doublesang BigDecimaltrong Java.

Vì đây là một câu hỏi bất khả tri về ngôn ngữ, nên nó cần các công cụ bất khả tri về ngôn ngữ, chẳng hạn như Công cụ chuyển đổi từ thập phân sang dấu chấm động .

Áp dụng nó cho các số trong câu hỏi, được coi là nhân đôi:

0,1 chuyển đổi thành 0,1000000000000000055511151231257827021181583404541015625,

0,2 chuyển đổi thành 0,200000000000000011102230246251565404236316680908203125,

0,3 chuyển đổi thành 0,299999999999999988897769753748434595763683319091796875 và

0,30000000000000004 chuyển đổi thành 0,3000000000000000444089209850062616169452667236328125.

Việc thêm hai số đầu tiên theo cách thủ công hoặc trong máy tính thập phân như Máy tính chính xác đầy đủ , sẽ hiển thị tổng chính xác của các đầu vào thực tế là 0,3000000000000000166533453693773481063544750213623046875.

Nếu nó được làm tròn xuống tương đương với 0,3 thì sai số làm tròn sẽ là 0,0000000000000000277555756156289135105907917022705078125. Làm tròn lên đến tương đương với 0,30000000000000004 cũng cho lỗi làm tròn 0,0000000000000000277555756156289135105907917022705078125. Áp dụng bộ ngắt cà vạt từ tròn đến chẵn.

Quay trở lại công cụ chuyển đổi dấu phẩy động, hệ thập lục phân thô cho 0,30000000000000004 là 3fd3333333333334, kết thúc bằng chữ số chẵn và do đó là kết quả chính xác.

15 Noname Mar 18 2016 at 07:38

Tôi có thể chỉ thêm; mọi người luôn cho rằng đây là một vấn đề máy tính, nhưng nếu bạn đếm bằng tay (cơ số 10), bạn không thể nhận được (1/3+1/3=2/3)=truetrừ khi bạn có vô cùng để thêm 0,333 ... thành 0,333 ... vì vậy cũng giống như (1/10+2/10)!==3/10bài toán trong cơ số 2, bạn cắt ngắn nó thành 0,333 + 0,333 = 0,666 và có thể làm tròn nó thành 0,667, điều này cũng sẽ không chính xác về mặt kỹ thuật.

Đếm trong bậc ba và phần ba không phải là một vấn đề - có thể một số chủng tộc với 15 ngón tay trên mỗi bàn tay sẽ hỏi tại sao phép toán thập phân của bạn bị hỏng ...

9 BlairHoughton Oct 05 2015 at 22:55

Loại toán dấu phẩy động có thể được thực hiện trong máy tính kỹ thuật số nhất thiết phải sử dụng phép tính gần đúng của các số thực và các phép toán trên chúng. ( Phiên bản tiêu chuẩn có hơn năm mươi trang tài liệu và có một ủy ban xử lý lỗi và cải tiến thêm.)

Giá trị gần đúng này là một hỗn hợp của các giá trị gần đúng khác nhau, mỗi giá trị có thể bị bỏ qua hoặc được tính toán cẩn thận do cách thức cụ thể của độ lệch so với độ cao. Nó cũng liên quan đến một số trường hợp ngoại lệ rõ ràng ở cả cấp độ phần cứng và phần mềm mà hầu hết mọi người đi qua trong khi giả vờ không nhận thấy.

Nếu bạn cần độ chính xác vô hạn (ví dụ: sử dụng số π thay vì một trong nhiều giá trị ngắn hơn của nó), bạn nên viết hoặc sử dụng một chương trình toán học biểu tượng để thay thế.

Nhưng nếu bạn đồng ý với ý tưởng rằng đôi khi toán học dấu phẩy động có giá trị mờ nhạt và logic và lỗi có thể tích lũy nhanh chóng và bạn có thể viết các yêu cầu và thử nghiệm của mình để cho phép điều đó, thì mã của bạn thường có thể xử lý được với những gì trong FPU của bạn.

9 alinsoar Dec 29 2016 at 17:29

Chỉ để giải trí, tôi đã chơi với biểu diễn của phao, theo các định nghĩa từ Tiêu chuẩn C99 và tôi đã viết mã bên dưới.

Mã in ra biểu diễn nhị phân của float trong 3 nhóm riêng biệt

SIGN EXPONENT FRACTION

và sau đó nó in ra một tổng, khi được tổng với đủ độ chính xác, nó sẽ hiển thị giá trị thực sự tồn tại trong phần cứng.

Vì vậy, khi bạn viết float x = 999..., trình biên dịch sẽ biến đổi số đó dưới dạng biểu diễn bit được in bởi hàm xxsao cho tổng được hàm in ra yybằng số đã cho.

Trong thực tế, tổng này chỉ là một con số gần đúng. Đối với số 999.999.999 trình biên dịch sẽ chèn vào biểu diễn bit của số float là 1.000.000.000

Sau khi mã, tôi đính kèm một phiên giao diện điều khiển, trong đó tôi tính tổng các điều khoản cho cả hai hằng số (trừ PI và 999999999) thực sự tồn tại trong phần cứng, được trình biên dịch chèn vào đó.

#include <stdio.h>
#include <limits.h>

void
xx(float *x)
{
    unsigned char i = sizeof(*x)*CHAR_BIT-1;
    do {
        switch (i) {
        case 31:
             printf("sign:");
             break;
        case 30:
             printf("exponent:");
             break;
        case 23:
             printf("fraction:");
             break;

        }
        char b=(*(unsigned long long*)x&((unsigned long long)1<<i))!=0;
        printf("%d ", b);
    } while (i--);
    printf("\n");
}

void
yy(float a)
{
    int sign=!(*(unsigned long long*)&a&((unsigned long long)1<<31));
    int fraction = ((1<<23)-1)&(*(int*)&a);
    int exponent = (255&((*(int*)&a)>>23))-127;

    printf(sign?"positive" " ( 1+":"negative" " ( 1+");
    unsigned int i = 1<<22;
    unsigned int j = 1;
    do {
        char b=(fraction&i)!=0;
        b&&(printf("1/(%d) %c", 1<<j, (fraction&(i-1))?'+':')' ), 0);
    } while (j++, i>>=1);

    printf("*2^%d", exponent);
    printf("\n");
}

void
main()
{
    float x=-3.14;
    float y=999999999;
    printf("%lu\n", sizeof(x));
    xx(&x);
    xx(&y);
    yy(x);
    yy(y);
}

Đây là một phiên giao diện điều khiển trong đó tôi tính toán giá trị thực của float tồn tại trong phần cứng. Tôi đã sử dụng bcđể in tổng các điều khoản được xuất ra bởi chương trình chính. Người ta có thể chèn số tiền đó trong python replhoặc một cái gì đó tương tự cũng được.

-- .../terra1/stub
@ qemacs f.c
-- .../terra1/stub
@ gcc f.c
-- .../terra1/stub
@ ./a.out
sign:1 exponent:1 0 0 0 0 0 0 fraction:0 1 0 0 1 0 0 0 1 1 1 1 0 1 0 1 1 1 0 0 0 0 1 1
sign:0 exponent:1 0 0 1 1 1 0 fraction:0 1 1 0 1 1 1 0 0 1 1 0 1 0 1 1 0 0 1 0 1 0 0 0
negative ( 1+1/(2) +1/(16) +1/(256) +1/(512) +1/(1024) +1/(2048) +1/(8192) +1/(32768) +1/(65536) +1/(131072) +1/(4194304) +1/(8388608) )*2^1
positive ( 1+1/(2) +1/(4) +1/(16) +1/(32) +1/(64) +1/(512) +1/(1024) +1/(4096) +1/(16384) +1/(32768) +1/(262144) +1/(1048576) )*2^29
-- .../terra1/stub
@ bc
scale=15
( 1+1/(2) +1/(4) +1/(16) +1/(32) +1/(64) +1/(512) +1/(1024) +1/(4096) +1/(16384) +1/(32768) +1/(262144) +1/(1048576) )*2^29
999999999.999999446351872

Đó là nó. Giá trị của 999999999 trên thực tế là

999999999.999999446351872

Bạn cũng có thể kiểm tra với bc-3.14 cũng bị xáo trộn. Đừng quên đặt một scaleyếu tố trong bc.

Tổng được hiển thị là những gì bên trong phần cứng. Giá trị bạn thu được bằng cách tính toán nó phụ thuộc vào quy mô bạn đặt. Tôi đã đặt scalehệ số là 15. Về mặt toán học, với độ chính xác vô hạn, có vẻ như nó là 1.000.000.000.

5 TorstenBecker Dec 20 2017 at 05:37

Một cách khác để xem xét điều này: Được sử dụng là 64 bit để biểu diễn số. Do đó, không có cách nào nhiều hơn 2 ** 64 = 18.446.744.073.709.551.616 số khác nhau có thể được biểu diễn chính xác.

Tuy nhiên, Math cho biết đã có vô số số thập phân từ 0 đến 1. IEE 754 xác định một mã hóa để sử dụng hiệu quả 64 bit này cho một không gian số lớn hơn nhiều cộng với NaN và +/- Infinity, vì vậy có khoảng cách giữa các số được biểu diễn chính xác được lấp đầy bởi con số chỉ gần đúng.

Thật không may, 0,3 nằm trong một khoảng trống.

5 nauer Aug 08 2018 at 15:47

Kể từ Python 3.5, bạn có thể sử dụng math.isclose()hàm để kiểm tra bình đẳng gần đúng:

>>> import math
>>> math.isclose(0.1 + 0.2, 0.3)
True
>>> 0.1 + 0.2 == 0.3
False
4 DanielMcLaury Dec 21 2018 at 01:27

Hãy tưởng tượng làm việc trong cơ số mười với, ví dụ, 8 chữ số chính xác. Bạn kiểm tra xem

1/3 + 2 / 3 == 1

và biết rằng điều này sẽ trở lại false. Tại sao? Chà, như những con số thực chúng ta có

1/3 = 0,333 ....2/3 = 0,666 ....

Cắt bớt tám chữ số thập phân, chúng tôi nhận được

0.33333333 + 0.66666666 = 0.99999999

tất nhiên là khác với 1.00000000chính xác 0.00000001.


Tình huống đối với các số nhị phân với một số bit cố định là hoàn toàn tương tự. Là số thực, chúng tôi có

1/10 = 0,0001100110011001100 ... (cơ số 2)

1/5 = 0,0011001100110011001 ... (cơ số 2)

Nếu chúng ta cắt ngắn những thứ này thành, chẳng hạn, bảy bit, thì chúng ta sẽ nhận được

0.0001100 + 0.0011001 = 0.0100101

trong khi mặt khác,

3/10 = 0,01001100110011 ... (cơ số 2)

mà, được cắt ngắn thành bảy bit, là 0.0100110, và chúng khác nhau chính xác 0.0000001.


Tình hình chính xác hơi phức tạp hơn vì những con số này thường được lưu trữ trong ký hiệu khoa học. Vì vậy, ví dụ, thay vì lưu trữ 1/10 như 0.0001100chúng ta có thể lưu trữ nó dưới dạng tương tự 1.10011 * 2^-4, tùy thuộc vào số lượng bit mà chúng ta đã phân bổ cho số mũ và phần định trị. Điều này ảnh hưởng đến số lượng chữ số chính xác bạn nhận được cho các phép tính của mình.

Kết quả là do những lỗi làm tròn này về cơ bản, bạn không bao giờ muốn sử dụng == trên các số dấu phẩy động. Thay vào đó, bạn có thể kiểm tra xem giá trị tuyệt đối của sự khác biệt có nhỏ hơn một số nhỏ cố định nào đó hay không.

4 chqrlie Apr 22 2019 at 08:02

Các số thập phân chẳng hạn như 0.1, 0.20.3không được biểu diễn chính xác trong các loại dấu phẩy động được mã hóa nhị phân. Tổng của các giá trị gần đúng 0.10.2khác với giá trị gần đúng được sử dụng cho 0.3, do đó, sự sai lệch của 0.1 + 0.2 == 0.3như có thể được nhìn thấy rõ ràng hơn ở đây:

#include <stdio.h>

int main() {
    printf("0.1 + 0.2 == 0.3 is %s\n", 0.1 + 0.2 == 0.3 ? "true" : "false");
    printf("0.1 is %.23f\n", 0.1);
    printf("0.2 is %.23f\n", 0.2);
    printf("0.1 + 0.2 is %.23f\n", 0.1 + 0.2);
    printf("0.3 is %.23f\n", 0.3);
    printf("0.3 - (0.1 + 0.2) is %g\n", 0.3 - (0.1 + 0.2));
    return 0;
}

Đầu ra:

0.1 + 0.2 == 0.3 is false
0.1 is 0.10000000000000000555112
0.2 is 0.20000000000000001110223
0.1 + 0.2 is 0.30000000000000004440892
0.3 is 0.29999999999999998889777
0.3 - (0.1 + 0.2) is -5.55112e-17

Để các phép tính này được đánh giá một cách đáng tin cậy hơn, bạn sẽ cần sử dụng cách biểu diễn dựa trên số thập phân cho các giá trị dấu phẩy động. Tiêu chuẩn C không chỉ định các loại như vậy theo mặc định mà là một phần mở rộng được mô tả trong Báo cáo kỹ thuật .

Các _Decimal32, _Decimal64_Decimal128loại có thể có sẵn trên hệ thống của bạn (ví dụ, GCC hỗ trợ họ về các mục tiêu được lựa chọn , nhưng Clang không hỗ trợ họ trên OS X ).

3 Piedone Dec 22 2017 at 23:39

Vì chủ đề này phân nhánh một chút thành một cuộc thảo luận chung về các triển khai dấu phẩy động hiện tại, tôi muốn thêm rằng có các dự án về việc khắc phục sự cố của họ.

Hãy xem https://posithub.org/ví dụ, trong đó giới thiệu một loại số được gọi là posit (và tiền thân của nó là unum) hứa hẹn cung cấp độ chính xác tốt hơn với ít bit hơn. Nếu sự hiểu biết của tôi là đúng, nó cũng khắc phục được loại vấn đề trong câu hỏi. Dự án khá thú vị, người đứng sau nó là một nhà toán học, Tiến sĩ John Gustafson . Toàn bộ điều là mã nguồn mở, với nhiều triển khai thực tế trong C / C ++, Python, Julia và C # (https://hastlayer.com/arithmetics).

3 VladAgurets May 08 2019 at 02:45

Nó thực sự khá đơn giản. Khi bạn có một hệ cơ số 10 (như của chúng tôi), nó chỉ có thể biểu thị các phân số sử dụng một thừa số nguyên tố của cơ số. Các thừa số nguyên tố của 10 là 2 và 5. Vì vậy, 1/2, 1/4, 1/5, 1/8 và 1/10 đều có thể được biểu thị rõ ràng vì các mẫu số đều sử dụng các thừa số nguyên tố là 10. Ngược lại, 1 / 3, 1/6 và 1/7 đều là số thập phân lặp lại vì mẫu số của chúng sử dụng thừa số nguyên tố là 3 hoặc 7. Trong hệ nhị phân (hoặc cơ số 2), thừa số nguyên tố duy nhất là 2. Vì vậy, bạn chỉ có thể biểu thị các phân số một cách sạch sẽ. chỉ chứa 2 như một thừa số nguyên tố. Trong hệ nhị phân, 1/2, 1/4, 1/8 sẽ được biểu thị rõ ràng dưới dạng số thập phân. Trong khi, 1/5 hoặc 1/10 sẽ lặp lại các số thập phân. Vì vậy, 0,1 và 0,2 (1/10 và 1/5) trong khi các số thập phân sạch trong hệ cơ số 10, đang lặp lại các số thập phân trong hệ cơ số 2 mà máy tính đang vận hành. Khi bạn làm toán trên các số thập phân lặp lại này, bạn sẽ nhận được phần thừa sẽ được tiếp tục khi bạn chuyển đổi số cơ số 2 (nhị phân) của máy tính thành số cơ sở 10 dễ đọc hơn của con người.

Từ https://0.30000000000000004.com/

2 RollerSimmer Aug 20 2020 at 22:38

Số học bình thường là cơ số 10, vì vậy số thập phân đại diện cho phần mười, phần trăm, v.v. Khi bạn cố gắng biểu diễn một số dấu phẩy động trong số học nhị phân cơ số 2, bạn đang xử lý các nửa, phần tư, phần tám, v.v.

Trong phần cứng, các dấu chấm động được lưu trữ dưới dạng phần định trị số nguyên và số mũ. Mantissa đại diện cho các chữ số có nghĩa. Số mũ giống như ký hiệu khoa học nhưng nó sử dụng cơ số 2 thay vì 10. Ví dụ 64.0 sẽ được biểu diễn với phần định trị là 1 và số mũ là 6. 0,125 sẽ được biểu diễn với phần định trị là 1 và số mũ là -3.

Số thập phân dấu phẩy động phải cộng lũy ​​thừa âm của 2

0.1b = 0.5d
0.01b = 0.25d
0.001b = 0.125d
0.0001b = 0.0625d
0.00001b = 0.03125d

và như thế.

Người ta thường sử dụng một delta lỗi thay vì sử dụng các toán tử bình đẳng khi xử lý số học dấu phẩy động. Thay vì

if(a==b) ...

bạn sẽ sử dụng

delta = 0.0001; // or some arbitrarily small amount
if(a - b > -delta && a - b < delta) ...
1 TheMachinist Aug 03 2020 at 22:03

Số dấu phẩy động được biểu diễn, ở cấp độ phần cứng, dưới dạng phân số của số nhị phân (cơ số 2). Ví dụ, phân số thập phân:

0.125

có giá trị 1/10 + 2/100 + 5/1000 và theo cách tương tự, phân số nhị phân:

0.001

có giá trị 0/2 + 0/4 + 1/8. Hai phân số này có cùng giá trị, chỉ khác ở chỗ thứ nhất là phân số thập phân, phân số thứ hai là phân số nhị phân.

Thật không may, hầu hết các phân số thập phân không thể có biểu diễn chính xác trong phân số nhị phân. Do đó, nhìn chung, số dấu phẩy động bạn đưa ra chỉ gần đúng với phân số nhị phân cần lưu trong máy.

Bài toán dễ tiếp cận hơn trong cơ số 10. Lấy ví dụ, phân số 1/3. Bạn có thể ước tính nó thành một phân số thập phân:

0.3

hoặc tốt hơn,

0.33

hoặc tốt hơn,

0.333

vv Cho dù bạn viết bao nhiêu chữ số thập phân, kết quả không bao giờ chính xác là 1/3, nhưng nó là một ước tính luôn đến gần hơn.

Tương tự như vậy, bất kể bạn sử dụng bao nhiêu chữ số thập phân cơ số 2, giá trị thập phân 0,1 không thể được biểu diễn chính xác dưới dạng phân số nhị phân. Trong cơ số 2, 1/10 là số tuần hoàn sau:

0.0001100110011001100110011001100110011001100110011 ...

Dừng lại ở bất kỳ số lượng bit hữu hạn nào, và bạn sẽ nhận được giá trị gần đúng.

Đối với Python, trên một máy thông thường, 53 bit được sử dụng cho độ chính xác của một số float, vì vậy giá trị được lưu trữ khi bạn nhập số thập phân 0,1 là phân số nhị phân.

0.00011001100110011001100110011001100110011001100110011010

gần, nhưng không chính xác bằng 1/10.

Thật dễ dàng để quên rằng giá trị được lưu trữ là một giá trị gần đúng của phân số thập phân ban đầu, do cách các số nổi được hiển thị trong trình thông dịch. Python chỉ hiển thị giá trị gần đúng thập phân của giá trị được lưu trữ trong hệ nhị phân. Nếu Python xuất ra giá trị thập phân thực của xấp xỉ nhị phân được lưu trữ cho 0,1, nó sẽ xuất ra:

>>> 0.1
0.1000000000000000055511151231257827021181583404541015625

Đây là số chữ số thập phân nhiều hơn nhiều so với hầu hết mọi người mong đợi, vì vậy Python hiển thị một giá trị được làm tròn để cải thiện khả năng đọc:

>>> 0.1
0.1

Điều quan trọng là phải hiểu rằng trong thực tế đây là một ảo ảnh: giá trị được lưu trữ không chính xác là 1/10, nó chỉ đơn giản là trên màn hình rằng giá trị được lưu trữ được làm tròn. Điều này trở nên rõ ràng ngay sau khi bạn thực hiện các phép toán số học với các giá trị sau:

>>> 0.1 + 0.2
0.30000000000000004

Hành vi này vốn có trong bản chất của biểu diễn dấu phẩy động của máy: nó không phải là lỗi trong Python, cũng không phải là lỗi trong mã của bạn. Bạn có thể quan sát cùng một kiểu hành vi trong tất cả các ngôn ngữ khác sử dụng hỗ trợ phần cứng để tính số dấu phẩy động (mặc dù một số ngôn ngữ không làm cho sự khác biệt hiển thị theo mặc định hoặc không hiển thị trong tất cả các chế độ hiển thị).

Một bất ngờ khác là cố hữu trong điều này. Ví dụ: nếu bạn cố gắng làm tròn giá trị 2,675 thành hai chữ số thập phân, bạn sẽ nhận được

>>> round (2.675, 2)
2.67

Tài liệu cho vòng () nguyên thủy chỉ ra rằng nó làm tròn đến giá trị gần nhất từ ​​0. Vì phân số thập phân nằm chính xác một nửa giữa 2,67 và 2,68, bạn sẽ nhận được (xấp xỉ nhị phân của) 2,68. Tuy nhiên, đây không phải là trường hợp vì khi phần thập phân 2.675 được chuyển đổi thành số thực, nó được lưu trữ bằng một giá trị gần đúng có giá trị chính xác là:

2.67499999999999982236431605997495353221893310546875

Vì giá trị gần đúng hơn một chút với 2,67 so với 2,68, nên làm tròn số sẽ giảm xuống.

Nếu bạn đang ở trong tình huống làm tròn số thập phân xuống nửa chừng quan trọng, bạn nên sử dụng mô-đun thập phân. Nhân tiện, mô-đun thập phân cũng cung cấp một cách thuận tiện để "xem" giá trị chính xác được lưu trữ cho bất kỳ số float nào.

>>> from decimal import Decimal
>>> Decimal (2.675)
>>> Decimal ('2.67499999999999982236431605997495353221893310546875')

Một hệ quả khác của thực tế là 0,1 không được lưu trữ chính xác trong 1/10 là tổng của mười giá trị 0,1 cũng không cho 1,0:

>>> sum = 0.0
>>> for i in range (10):
... sum + = 0.1
...>>> sum
0.9999999999999999

Số học của các số dấu phẩy động nhị phân chứa đựng nhiều điều bất ngờ như vậy. Vấn đề với "0,1" được giải thích chi tiết bên dưới, trong phần "Lỗi đại diện". Xem Nguy cơ của Điểm nổi để có danh sách đầy đủ hơn về những điều bất ngờ như vậy.

Đúng là không có câu trả lời đơn giản, tuy nhiên đừng quá nghi ngờ về những con số ảo trôi nổi! Các lỗi, trong Python, trong các phép toán số dấu phẩy động là do phần cứng bên dưới và trên hầu hết các máy không nhiều hơn 1 trong 2 ** 53 cho mỗi lần hoạt động. Điều này là không cần thiết đối với hầu hết các tác vụ, nhưng bạn nên nhớ rằng đây không phải là các phép toán thập phân và mọi hoạt động trên số dấu phẩy động có thể bị lỗi mới.

Mặc dù tồn tại các trường hợp bệnh lý, nhưng đối với hầu hết các trường hợp sử dụng phổ biến, bạn sẽ nhận được kết quả mong đợi ở cuối bằng cách chỉ cần làm tròn đến số chữ số thập phân bạn muốn trên màn hình. Để kiểm soát tốt cách hiển thị các số nổi, hãy xem Cú pháp định dạng chuỗi để biết các thông số kỹ thuật định dạng của phương thức str.format ().

Phần này của câu trả lời giải thích chi tiết ví dụ về "0,1" và chỉ ra cách bạn có thể tự mình thực hiện phân tích chính xác loại trường hợp này. Chúng tôi giả định rằng bạn đã quen thuộc với biểu diễn nhị phân của số dấu phẩy động. Thuật ngữ Lỗi biểu diễn có nghĩa là hầu hết các phân số thập phân không thể được biểu diễn chính xác trong hệ nhị phân. Đây là lý do chính tại sao Python (hoặc Perl, C, C ++, Java, Fortran và nhiều thứ khác) thường không hiển thị kết quả chính xác ở dạng thập phân:

>>> 0.1 + 0.2
0.30000000000000004

Tại sao ? 1/10 và 2/10 không thể biểu diễn chính xác dưới dạng phân số nhị phân. Tuy nhiên, tất cả các máy ngày nay (tháng 7 năm 2010) đều tuân theo tiêu chuẩn IEEE-754 về số học các số dấu phẩy động. và hầu hết các nền tảng sử dụng "độ chính xác kép IEEE-754" để biểu thị số float của Python. Độ chính xác kép IEEE-754 sử dụng độ chính xác 53 bit, vì vậy khi đọc máy tính sẽ cố gắng chuyển đổi 0,1 thành phần gần nhất của dạng J / 2 ** N với J thành số nguyên chính xác 53 bit. Viết lại:

1/10 ~ = J / (2 ** N)

trong :

J ~ = 2 ** N / 10

nhớ rằng J chính xác là 53 bit (vì vậy> = 2 ** 52 nhưng <2 ** 53), giá trị tốt nhất có thể cho N là 56:

>>> 2 ** 52
4503599627370496
>>> 2 ** 53
9007199254740992
>>> 2 ** 56/10
7205759403792793

Vì vậy, 56 là giá trị duy nhất có thể cho N mà để lại chính xác 53 bit cho J. Giá trị tốt nhất có thể có cho J là thương số này, được làm tròn:

>>> q, r = divmod (2 ** 56, 10)
>>> r
6

Vì giá trị mang lớn hơn một nửa của 10, giá trị gần đúng nhất thu được bằng cách làm tròn:

>>> q + 1
7205759403792794

Do đó, giá trị gần đúng nhất có thể cho 1/10 trong "IEEE-754 double precision" là giá trị trên 2 ** 56, nghĩa là:

7205759403792794/72057594037927936

Lưu ý rằng kể từ khi làm tròn được thực hiện trở lên, kết quả thực sự lớn hơn một chút so với 1/10; nếu chúng ta không làm tròn, thương số sẽ nhỏ hơn 1/10 một chút. Nhưng không có trường hợp nào nó chính xác là 1/10!

Vì vậy, máy tính không bao giờ "nhìn thấy" 1/10: những gì nó nhìn thấy là phân số chính xác được đưa ra ở trên, xấp xỉ tốt nhất sử dụng các số dấu phẩy động có độ chính xác kép từ "" IEEE-754 ":

>>>. 1 * 2 ** 56
7205759403792794.0

Nếu chúng ta nhân phân số này với 10 ** 30, chúng ta có thể quan sát các giá trị của 30 chữ số thập phân của nó.

>>> 7205759403792794 * 10 ** 30 // 2 ** 56
100000000000000005551115123125L

nghĩa là giá trị chính xác được lưu trữ trong máy tính xấp xỉ bằng giá trị thập phân 0,100000000000000005551115123125. Trong các phiên bản trước Python 2.7 và Python 3.1, Python đã làm tròn các giá trị này thành 17 chữ số thập phân quan trọng, hiển thị “0,10000000000000001”. Trong các phiên bản Python hiện tại, giá trị được hiển thị là giá trị có phân số càng ngắn càng tốt trong khi cung cấp chính xác cùng một biểu diễn khi được chuyển đổi lại thành nhị phân, chỉ cần hiển thị “0,1”.

costargc Oct 06 2019 at 04:46

Tôi vừa thấy vấn đề thú vị này xung quanh các điểm nổi:

Hãy xem xét các kết quả sau:

error = (2**53+1) - int(float(2**53+1))
>>> (2**53+1) - int(float(2**53+1))
1

Chúng ta có thể thấy rõ ràng một điểm ngắt khi 2**53+1- tất cả đều hoạt động tốt cho đến khi 2**53.

>>> (2**53) - int(float(2**53))
0

Điều này xảy ra do nhị phân chính xác kép: Định dạng dấu chấm động nhị phân chính xác kép IEEE 754: binary64

Từ trang Wikipedia về định dạng dấu phẩy động chính xác kép :

Dấu phẩy động nhị phân độ chính xác kép là định dạng thường được sử dụng trên PC, do phạm vi rộng hơn so với dấu chấm động chính xác đơn, bất chấp chi phí về hiệu suất và băng thông. Như với định dạng dấu phẩy động chính xác đơn, nó thiếu độ chính xác đối với số nguyên khi so sánh với định dạng số nguyên có cùng kích thước. Nó thường được gọi đơn giản là gấp đôi. Tiêu chuẩn IEEE 754 chỉ định binary64 là có:

  • Ký hiệu bit: 1 bit
  • Số mũ: 11 bit
  • Độ chính xác đáng kể: 53 bit (52 được lưu trữ rõ ràng)

Giá trị thực được giả định bởi dữ liệu độ chính xác kép 64 bit nhất định với số mũ chệch cho trước và phân số 52 bit là

hoặc là

Cảm ơn @a_guest đã chỉ ra điều đó cho tôi.