Toán học dấu phẩy động có bị hỏng không?
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
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.1
chuẩ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ặc0x1.999999999999ap-4
trong 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ặc0x1.99999999999999...p-4
trong 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.2
và 0.3
trong 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.2
nhưng điều đó gần nhất double
để 0.3
là nhỏ hơn so với số lượng hợp lý 0.3
. Tổng 0.1
và 0.2
kết thúc lớn hơn số hữu tỉ 0.3
và 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 abs
là giá trị tuyệt đối. myToleranceValue
cầ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.
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= 4
cá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.
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.
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.)
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.
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.3
trả về false
trong 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: 2550
xu thay vì 25.50
USD.
1 Douglas Crockford: JavaScript: Những phần hay : Phụ lục A - Những phần đáng kinh ngạc (trang 105) .
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 :
1
nếu là số âm,0
ngượ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
và -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 1
s) 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ị a và b , 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 a và b 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.2
bằ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.2
sẽ cung cấp 0.3
: một số bit cuối cùng được làm tròn.
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.1
sẽ 1 x 10⁻¹
, 0.2
sẽ 2 x 10⁻¹
và 0.3
sẽ 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.2
hiể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.5
là 1 x 2⁻¹
và 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.1
là 3602879701896397 x 2⁻⁵⁵
và số gần nhất với 0.2
là 7205759403792794 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ị.
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.
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.
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.2
dễ dàng, FLT_RADIX
và DBL_RADIX
là 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.
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%).
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 là 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 đó.
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.2
và 0.7
ở đây nó là: 0.2 + 0.7 = 0.8999999999999999
.
Kết quả mong đợi của bạn là 0.9
nó 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 / 10
sẽ 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.
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 **
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).
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
decimal
Mô-đ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.
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 float
hoặc double
sang BigDecimal
trong 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.
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)=true
trừ 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/10
bà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 ...
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.
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 xx
sao cho tổng được hàm in ra yy
bằ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 repl
hoặ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 scale
yế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 scale
hệ 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.
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.
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
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 .... và 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.00000000
chí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)
và
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.0001100
chú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.
Các số thập phân chẳng hạn như 0.1
, 0.2
và 0.3
khô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.1
và 0.2
khá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.3
như 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
và _Decimal128
loạ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 ).
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).
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/
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) ...
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”.
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.