Hãy xem xét các mã sau đây:
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 xảy ra?
Hãy xem xét các mã sau đây:
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 xảy ra?
Câu trả lời:
Toán học 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 . Mấu chốt của vấn đề là các số được biểu diễn theo định dạng này dưới dạng toàn bộ số nhân với hai lũy thừa; 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
tiêu 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 tương tự của ký hiệu hexfloat C99, trong đó ...
đại diện cho một chuỗi 9 không có hồi kết.Các hằng số 0.2
và 0.3
trong chương trình của bạn cũng sẽ gần đúng với các 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à 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à mọi nhà khoa học máy tính nên biết về số học dấu phẩy động . Để biết giải thích dễ tiêu hóa hơn, hãy xem float-point-gui.de .
Lưu ý bên lề: Tất cả các hệ thống số vị trí (cơ sở N) chia sẻ vấn đề này với độ chính xác
Các số thập phân cũ (cơ số 10) có cùng các vấn đề, đó là lý do tại sao các số như 1/3 kết thúc là 0,33333333 ...
Bạn vừa vấp phải một con số (3/10) có thể dễ dàng biểu diễn với hệ thập phân, nhưng không phù hợp với hệ thống nhị phân. Nó cũng đi cả hai chiều (ở một mức độ nhỏ): 1/16 là một con số xấu xí trong số thập phân (0,0625), nhưng ở dạng nhị phân, nó trông gọn gàng như một số 10.000 trong số thập phân (0,0001) ** - nếu chúng ta ở trong 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, thậm chí bạn sẽ nhìn vào số đó và hiểu theo bản năng bạn có thể đến đó bằng cách giảm một nửa thứ gì đó, giảm một lần nữa và lặp đi lặp lại.
** Tất nhiên, đó không chính xác là cách cá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 điểm mà các lỗi chính xác của dấu phẩy động nhị phân có xu hướng tăng lên vì các số "thế giới thực" mà chúng ta thường quan tâm khi làm việc thường là lũy thừa mười - nhưng chỉ vì chúng ta sử dụng hệ thống số thập phân ngày- hôm nay. Đây cũng là lý do tại sao chúng tôi sẽ nói những thứ như 71% thay vì "5 trên 7" (71% là xấp xỉ, 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ị phá vỡ, 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
Trong thực tế, vấn đề 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 mình thành nhiều vị trí 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ố lượng 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) { ... }
.
đâu abs
là giá trị tuyệt đối. myToleranceValue
cần phải được chọn cho ứng dụng cụ thể của bạn - và nó sẽ ảnh hưởng rất nhiều đến số lượng "phòng ngọ nguậy" mà bạn chuẩn bị cho phép, và con số lớn nhất bạn sẽ so sánh có thể là gì (do mất các vấn đề chính xác ). Cảnh giác với 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.
Tôi tin rằng tôi nên thêm phối cảnh của một nhà thiết kế phần cứng vào việc này vì tôi thiết kế và xây dựng phần cứng dấu phẩy động. Biết 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 phẩy động xảy ra và dường như tích lũy theo thời gian.
Từ góc độ kỹ thuật, hầu hết các hoạt động của dấu phẩy động sẽ có một số yếu tố lỗi do phần cứng thực hiện tính toán 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 ở độ chính xác chỉ cần thiết để gây ra lỗi 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 đặc biệt có vấn đề trong phân chia điểm nổi. Cái gì tạo thành một hoạt động đơn lẻ phụ thuộc vào số lượng toán hạng mà đơn vị thực hiện. Đối với hầu hết, nó là hai, nhưng một số đơn vị mất 3 toán hạng trở lên. Bởi vì điều này, không có gì đảm bảo rằng các hoạt động lặp đi lặp lại sẽ dẫn đến một lỗi mong muốn vì các lỗi cộng lại theo thời gian.
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 hóa không chuẩn hóa hoặc các tiêu chuẩn khác nhau. 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 đây sẽ bao gồm chế độ chuẩn hóa của IEEE-754, đây 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 có bất kỳ giá trị lỗi / epsilon nào 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 ít hơn một nửa của một đơn vị ở lần cuối nơi cho một hoạt động. Điều này giải thích tại sao khi có các hoạt động lặp đi lặp lại, các lỗi cộng lại. Đối với độ chính xác kép của IEEE-754, đây là bit thứ 54, vì 53 bit được sử dụng để biểu diễn phần số (được chuẩn hóa), còn được gọi là mantissa, của số dấu phẩy động (ví dụ: 5,3 trong 5,3e5). Các phần tiếp theo sẽ đi sâu vào chi tiết hơn về các nguyên nhân gây ra lỗi phần cứng trên các hoạt động của dấu phẩy động khác nhau.
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)
. Một phép chia được tính toán lặp đi lặp lại, tức là mỗi chu kỳ sẽ 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 cứ điều gì có sai số nhỏ hơn một đơn vị ở vị trí cuối cùng. Bảng đối ứng của Y (1 / Y) được gọi là bảng lựa chọn thương số (QST) trong phân chia chậm và kích thước tính theo bit của bảng lựa chọn thương số 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 toán 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, trong đó k>=2
. Vì vậy, ví dụ, Bảng lựa chọn Quotient điển hình cho một bộ chia tính toán 2 bit của thương số 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 Lỗi làm tròn bộ phận: Xấp xỉ đối ứng
Những đối ứng nào trong bảng lựa chọn thương số phụ thuộc vào phương pháp phân chia : phân chia chậm như phân chia SRT hoặc phân chia nhanh như phân chia Goldschmidt; mỗi mục đượ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 mọi trường hợp, tất cả các đối ứng là xấp xỉcủa đối ứng thực tế và giới thiệu một số yếu tố lỗi. Cả hai phương pháp phân chia chậm và phân chia nhanh đều tính toán thương số lặp đi lặp lại, tức là một số bit của thương số được tính toán từng bước, sau đó kết quả được trừ khỏi cổ tức và bộ chia lặp lại các bước cho đến khi sai số nhỏ hơn một nửa đơn vị ở nơi cuối cùng. Các phương pháp phân chia chậm tính toán một số chữ số cố định của thương số trong mỗi bước và thường ít tốn kém hơn để xây dựng và các phương pháp phân chia nhanh tính toán một số chữ số khác nhau trên mỗi bước và thường tốn kém hơn khi xây dựng. Phần quan trọng nhất của các phương pháp phân chia là hầu hết chúng dựa vào phép nhân lặp đi lặp lại bằng cách tính gần đúng của một đối ứng, do đó chúng dễ bị lỗi.
Một nguyên nhân khác của các lỗi làm tròn trong tất cả các hoạt động 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. Tất cả các phương pháp giới thiệu một yếu tố lỗi ít hơn một đơn vị ở vị trí cuối cùng cho một hoạt động. Theo thời gian và các hoạt động lặp đi lặp lại, cắt ngắn cũng thêm tích lũy cho lỗi kết quả. Lỗi cắt ngắn này đặc biệt có vấn đề trong lũy thừa, liên quan đến một số hình thức nhân lặp lại.
Do phần cứng thực hiện các phép tính dấu phẩy động chỉ cần tạo ra kết quả có 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, nên lỗi sẽ tăng lên trong các hoạt động lặp lại nếu không được xem. Đây là lý do trong các tính toán yêu cầu sai số 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ố 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 ngoài và Số học Interval kết hợp với các biến thể của chế độ làm tròn IEEE 754để dự đoán lỗi làm tròn, và sửa chúng. Do lỗi 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, chữ số tròn đến gần nhất ở vị trí cuối cùng , đảm bảo lỗi 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 cắt ngắn, làm tròn và làm tròn xuống có thể gây ra lỗi lớn hơn một nửa của một đơn vị ở vị trí cuối cùng, nhưng ít hơn một đơn vị ở vị trí cuối cùng, vì vậy các chế độ này không được khuyến khích trừ khi chúng là được sử dụng trong số học Interval.
Nói tóm lại, lý do cơ bản cho các lỗi trong các phép toán dấu phẩy động là sự kết hợp của việc cắt bớt trong phần cứng và cắt ngắn một đối ứng trong trường hợp phân chia. Do tiêu chuẩn IEEE-754 chỉ yêu cầu một lỗi 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, các lỗi dấu phẩy động trên các hoạt động lặp lại sẽ cộng trừ khi được sửa chữa.
Khi bạn chuyển đổi .1 hoặc 1/10 sang cơ sở 2 (nhị phân), bạn sẽ nhận được một mẫu lặp lại sau dấu thập phân, giống như cố gắng biểu thị 1/3 trong cơ sở 10. Giá trị không chính xác và do đó bạn không thể làm toán chính xác với nó bằng cách sử dụng các phương pháp dấu phẩy động bình thường.
Hầu hết các câu trả lời ở đây giải quyết câu hỏi này trong điều kiện kỹ thuật rất khô khan. Tôi muốn giải quyết vấn đề này theo cách 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 lát pizza. Bạn có một máy cắt pizza robot có thể cắt lát pizza chính xác một nửa. Nó có thể giảm một nửa toàn bộ bánh pizza, hoặc nó có thể giảm một nửa lát cắt hiện có, nhưng trong mọi trường hợp, một nửa luôn luôn chính xác.
Máy cắt pizza đó có chuyển động rất tốt và nếu bạn bắt đầu với toàn bộ bánh pizza, sau đó giảm một nửa và tiếp tục giảm một nửa lát nhỏ nhất mỗi lần, bạn có thể thực hiện giảm 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 còn có thể giảm một nửa lát cắt rất mỏng đó, 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 có thể thêm tới 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? Thực sự nghĩ về nó, và thử làm việc ra. 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 máy cắt pizza chính xác thần thoại trong tay. :-)
Tất cả các lập trình viên giàu kinh nghiệm, tất nhiên, biết câu trả lời thực sự, đó là không có cách nào để ghép một phần mười hoặc năm phần chính xác của bánh pizza bằng cách sử dụng những lát cắt đó, bất kể bạn cắt chúng một cách tinh xảo như thế nào. Bạn có thể thực hiện một xấp xỉ khá tốt và nếu bạn cộng xấp xỉ 0,1 với xấp xỉ 0,2, bạn sẽ có được xấp xỉ khá tốt là 0,3, nhưng vẫn chỉ là xấp xỉ.
Đố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 số pizza của mình xuống 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.1000000000000000055511151231257871515 Cái sau khá gần với 0,1 so với cái trước, do đó, một trình phân tích cú pháp số sẽ, với đầu vào là 0,1, thiên về cái sau.
(Sự khác biệt giữa hai số này là "lát cắt nhỏ nhất" mà chúng ta phải quyết định bao gồm, đưa ra xu hướng tăng hoặc loại trừ, đưa ra độ lệch xuống. Thuật ngữ kỹ thuật cho lát cắt nhỏ nhất đó là một ulp .)
Trong trường hợp 0,2, các số đều giống nhau, chỉ tăng theo hệ số 2. Một lần nữa, chúng tôi ủng hộ giá trị cao hơn 0,2 một chút.
Lưu ý rằng trong cả hai trường hợp, các xấp xỉ cho 0,1 và 0,2 có độ lệch tăng nhẹ. Nếu chúng ta thêm đủ các độ lệch này vào, chúng sẽ đẩy số càng ngày càng xa khỏ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 để số kết quả không còn là số gần nhất đến 0,3.
Cụ thể, 0.1 + 0.2 thực sự là 0.1000000000000000055511151231257827021181583404541015625 + 0.200000000000000011102230246251565404236316680908203125 = 000000000000000044404236316680908203125 = 0,3000000000000000444089209850062616169
PS Một số ngôn ngữ lập trình cũng cung cấp máy cắt pizza có thể chia lát thành các phần mười chính xác . Mặc dù máy cắt pizza như vậy là không phổ biến, nhưng nếu bạn có quyền truy cập vào một cái, bạn nên sử dụng nó khi điều quan trọng là có thể có được chính xác một phần mười hoặc một phần năm của một lát.
Lỗi làm tròn điểm nổi. 0,1 không thể được biểu diễn chính xác trong cơ sở 2 như trong cơ sở 10 do hệ số nguyên tố bị thiếu là 5. Cũng như 1/3 lấy một số chữ số vô hạn để biểu thị bằng số thập phân, nhưng là "0,1" trong cơ sở 3, 0,1 lấy một số lượng vô hạn các chữ số trong cơ sở-2 trong đó 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 nhân rộng 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 là số học số nguyên trong dấu phẩy động là chính xác, do đó có thể tránh được cá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 đề về dấu phẩy động trong đó độ chính xác là tối quan trọng, khuyến nghị 1 nên xử lý tiền dưới dạng một số nguyên biểu thị số xu: 2550
xu thay vì 25.50
đô la.
1 Douglas Crockford: JavaScript: Các bộ phận tốt : Phụ lục A - Các bộ phận khủng khiếp (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.
Lời nói đầu
Số định dạng dấu phẩy động nhị phân (binary64) chính xác kép của IEEE 754 đại diện cho một số dạng
giá trị = (-1) ^ s * (1.m 51 m 50 ... m 2 m 1 m 0 ) 2 * 2 e-1023
trong 64 bit:
1
nếu số âm, 0
nếu không thì 1 .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 về số 0 đã ký - +0
và -0
được đối xử khác nhau: 1 / (+0)
là vô cực dương; 1 / (-0)
là vô cực âm. Đối với các giá trị 0, các bit mantissa và lũy thừa đều bằng không. Lưu ý: giá trị 0 (+0 và -0) rõ ràng không được phân loại là không bình thường 2 .
2 - Đây không phải là trường hợp cho các số bất thường , có số mũ bù bằng 0 (và ngụ ý 0.
). Phạm vi của các số chính xác kép bất thường là d min x | x | Max 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, trong đó mantissa bao gồm toàn bộ 1
s) là 2 -1023 + 1 - 2 -1023 - 51 (≈ 2.225 * 10 -308 ).
Biến một số chính xác kép thành nhị phân
Nhiều bộ chuyển đổi trực tuyến tồn tại để chuyển đổi số dấu phẩy động chính xác kép thành nhị phân (ví dụ tại binaryconvert.com ), nhưng đây là một số mã C # mẫu để có được đại diện của IEEE 754 cho một số chính xác kép (tôi 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);
}
Đến điểm: câu hỏi ban đầu
(Bỏ qua phía dưới cho phiên bản TL; DR)
Cato Johnston (người 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 các dấu hai chấm phân tách ba phần), các biểu diễn của IEEE 754 của các giá trị là:
0.1 => 0:01111111011:1001100110011001100110011001100110011001100110011010
0.2 => 0:01111111100:1001100110011001100110011001100110011001100110011010
Lưu ý rằng lớp phủ bao gồm các chữ số định kỳ của 0011
. Đây là chìa khóa 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 nhị phân trong một số hữu hạn các bit nhị phân bất kỳ có thể vượt quá 1/9, 1/3 hoặc 1/7 chữ số thập phân .
Cũng lưu ý rằng chúng ta có thể giảm công suất theo số mũ xuống 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). Điều này sau đó cho phép chúng ta 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 thị dưới dạng * 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à thêm lại hàm ý 1
(trong 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
Để thêm hai số, số mũ cần phải giống nhau, nghĩa 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à thay đổi điểm thập phân ( nhị phân ) để có được:
sum = 2^-2 * 1.0011001100110011001100110011001100110011001100110011(1)
= 2^-54 * 5404319552844595.5 = 0.3000000000000000166533453693773481063544750213623046875
Hiện tại có 53 bit trong lớp phủ (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ị trong đó bit có ý nghĩa nhỏ nhất bằng 0 đượ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 bằng 2 -54 .
Biểu diễn nhị phân 0,1 và 0,2 là biểu diễn chính xác nhất của các số được phép bởi IEEE 754. Việc thêm 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 nhau ở bit có ý nghĩa nhỏ nhất.
TL; DR
Viết 0.1 + 0.2
một biểu diễn nhị phân của IEEE 754 (với các dấu hai chấm tách ba phần) và so sánh nó với 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]
Chuyển đổi trở lại 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 , tức 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 một vài bit cuối của số dấu phẩy động vốn đã nguy hiểm, vì bất kỳ ai đọc " Điều mà 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 để khắc phục vấn đề này, đó là cách 0.1 + 0.2
cung cấp 0.3
: một vài 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à số mũ mà cơ sở được lấy và nhân với phần nguyên.
Nếu máy tính 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⁻¹
. Toán học số nguyên là dễ dàng và chính xác, vì vậy việc thêm 0.1 + 0.2
rõ ràng sẽ dẫn đến 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ụ 0.5
là 1 x 2⁻¹
và 0.25
là 1 x 2⁻²
, và thêm chúng vào kết quả 3 x 2⁻²
, hoặc 0.75
. Chính xác.
Vấn đề đi kèm với các số có thể được biểu diễn chính xác trong cơ sở 10, nhưng không phải ở cơ sở 2. Những số đó cần được làm tròn đến tương đương gần nhất của chúng. Giả sử định dạng dấu phẩy động 64 bit rất phổ biến của IEEE, số gần nhất 0.1
là 3602879701896397 x 2⁻⁵⁵
và số gần nhất 0.2
là 7205759403792794 x 2⁻⁵⁵
; thêm 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 của 0.3000000000000000444089209850062616169452667236328125
. Số dấu phẩy động thường được làm tròn để hiển thị.
Lỗi làm tròn điểm nổi. Từ những gì mọi nhà khoa học máy tính nên biết về số học dấu phẩy động :
Việc ép vô số các số thực thành một số bit hữu hạn đòi hỏi 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ả tính toán 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 tính toán dấu phẩy độ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 nữa.
Không phải tất cả các số có thể được biểu diễn qua số float / double Ví dụ, số "0.2" sẽ được biểu diễn dưới dạng "0.200000003" với độ chính xác duy nhất trong tiêu chuẩn điểm nổi của IEEE754.
Mô hình để lưu trữ số thực dưới mui xe đại diện cho số float 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ột máy tính có FPU sử dụng "Tiêu chuẩn IEEE cho Số học dấu phẩy động nhị phân (ISO / IEEE Std 754-1985)".
Vì vậy, thật khó để thể hiện chính xác những con số như vậy. Ngay cả khi bạn chỉ định rõ ràng biến này mà không có bất kỳ tính toán 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 tôi có ~ 15% khả năng xảy ra lỗi chính xác . Lưu ý rằng lỗi có thể dẫn đến 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 tôi có ~ 34% khả năng xảy ra lỗi 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ự 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 trở nên tồi tệ hơn một chút (18% và 36%).
Tóm lược
Thật không may, 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ố 10 thông thường của chúng tôi, vì vậy hóa ra chúng tôi thường đưa ra đầu vào hơi khác so với những gì chúng tôi đã viết.
Ngay cả các 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 dưới dạng phân số nhị phân. Nếu bạn đếm 0,01, 0,02, 0,03 ..., cho đến khi bạn đạt 0,25, bạn sẽ nhận được phân số đầu tiên có thể biểu thị trong cơ sở 2 . Nếu bạn đã thử sử dụng FP, 0,01 của bạn sẽ bị tắt một chút, do đó, cách duy nhất để thêm 25 trong số chúng lên chính xác 0,25 sẽ yêu cầu một chuỗi quan hệ 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 giơ tay và nói "FP 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à một phần lặp lại trong cơ sở 2.
Làm sao chuyện này lại xảy ra?
Khi chúng tôi viết bằng số thập phân, mọi phân số (cụ thể, mỗi số thập phân kết thúc) là một số hữu tỷ của biểu mẫu
a / (2 n x 5 m )
Trong nhị phân, chúng tôi chỉ nhận được thuật ngữ 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 là 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ố 10 cơ sở. Tuy nhiên, hầu như bất cứ điều gì chúng ta viết dưới dạng phân số 10 cơ sở đều có thể biểu diễn dưới dạng 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 thị theo đị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 thuật ngữ 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 bằng số thập phân, thì điều ngược lại là không đúng. Và trong thực tế hầu hết các phân số thập phân lặp lại trong 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 < so sánh epsilon , lời khuyên tốt hơn có thể là làm tròn đế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, về các vấn đề khủng hoảng số thực (các vấn đề mà FP đã phát minh ra sớm trên các máy tính đắt tiền khủng khiếp) 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ủa các số liệu quan trọng, vì vậy toàn bộ không gian vấn đề dù sao cũng "không chính xác". FP "chính xác" không phải là một vấn đề trong loại ứng dụng này.
Toàn bộ vấn đề thực sự phát 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 tuân theo các giá trị tích phân, loại nào sẽ đánh bại điểm sử dụng nó. Đây là lý do tại sao chúng ta có tất cả các thư viện phần mềm 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à cách viết 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 ta có thể khắc phục điều đó và sẽ thực hiện nó từ nhiều thập kỷ trước. Lý do chúng tôi không có là vì định dạng FP nhỏ gọn và nhanh chóng và đó là cách tốt nhất để tạo ra nhiều con số. Ngoài ra, đó là một di sản từ thời đại vũ trụ 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 các hệ thống bộ nhớ nhỏ. (Đôi khi, các lõi từ 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 hạt đậ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 ở vị trí đầu tiên hoạt động hoàn toàn tốt. Nhưng bạn không thể thực hiện phương pháp sắc ký lượng tử hoặc khí động học theo cách đó.
nextafter()
với số nguyên tăng hoặc giảm trên biểu diễn nhị phân của số float. Ngoài ra, bạn có thể so sánh số float là số nguyên và nhận được câu trả lời đúng trừ khi cả hai đều âm (vì cường độ ký hiệu so với phần bù 2).
Bạn đã thử giải pháp băng keo?
Cố gắng xác định khi nào xảy ra lỗi và sửa chúng bằng các câu lệnh if ngắn, nó không đẹp nhưng đối với một số vấn đề thì đó 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 cánh bướm, nó sẽ biến thành một con rồng to béo và cắn bạn trong **
Để đưa ra giải pháp tốt nhất tôi có thể nói 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 nó là giải pháp tốt nhất. Như những câu hỏi khác được đề cập ở trên, bạn nên sử dụng hàm 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 sẽ thêm hai số float giống như 0.2
và 0.7
đây 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ả với độ 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ỉ đưa ra 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ố, vậy nó phải là tham toFixed(2)
số nào để phù hợp với mọi số float nhất định?
Bạn có thể nói hãy để nó là 10 trong mọi tình huống sau đó:
(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ố không mong muốn sau 9? Đã đến lúc chuyển đổi nó thành nổi để làm cho nó như bạn 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 là cung cấp nó như một chức năng như thế này:
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 cũng đề xuất 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 (0.2 + 0.1) * 10 / 10
sẽ không làm việc gì cả mặc dù có vẻ như vậy! Tôi thích giải pháp đầu tiên vì tôi có thể áp dụng nó như một chức năng chuyển đổi float đầu vào thành float đầu ra chính xác.
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 tôi sử dụng số thập phân (cơ sở 10).
Có phần lớn các số 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ả - Một kết quả số làm tròn (nhưng chính xác).
Nhiều câu hỏi trong số rất nhiều câu hỏi này hỏi về tác động của làm tròn điểm nổi lên các con số cụ thể. Trong thực tế, sẽ dễ dàng hơn để có được cảm giác về 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 tính toán quan tâm thay vì chỉ đọc về nó. Một số ngôn ngữ cung cấp các cách để làm điều đó - chẳng hạn như chuyển đổi một float
hoặc double
sang BigDecimal
Java.
Vì đây là một câu hỏi không liên quan đến ngôn ngữ, nó cần các công cụ không biết ngôn ngữ, chẳng hạn như Bộ chuyển đổi thập phân sang dấu phẩy động .
Áp dụng nó cho các số trong câu hỏi, được coi là gấp đô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.
Thêm hai số đầu tiên bằng tay hoặc trong một máy tính thập phân, chẳng hạn như Máy tính chính xác đầy đủ , 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 0,3 thì sai số làm tròn sẽ là 0,00000000000000277555756156289135105907917022705078125. Làm tròn số tương đương với 0,30000000000000004 cũng gây ra lỗi làm tròn 0,00000000000000277555756156289135105907917022705078125. Bộ ngắt kết nối tròn đến chẵn áp dụng.
Quay trở lại bộ 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 một chữ số chẵn và do đó là kết quả chính xác.
Cho rằng không 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 giới hạn dấu phẩy động nhị phân. Ví dụ:
decimal
Mô-đun Python và lớp JavaBigDecimal
, đạ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 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 đẹp khi giao dịch với tiền: mười xu cộng 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 .
fractions
Mô-đun của Python và BigFraction
lớp chung của Apache . Cả hai đều đại diện cho số hữu tỷ dưới dạng (numerator, denominator)
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.
Cả hai giải pháp này đều hoàn hảo (đặc biệt nếu chúng ta xem các màn trình 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.
Tôi chỉ có thể thêm; mọi người luôn cho rằng đây là sự cố 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ô số để thêm 0,33 ... đến 0,333 ... cũng giống như với sự (1/10+2/10)!==3/10
cố trong cơ sở 2, bạn cắt 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 không chính xác về mặt kỹ thuật.
Tuy nhiên, đếm ngược và phần ba không phải là 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 toán học thập phân của bạn bị hỏng ...
Loại toán học 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 xấp xỉ các số thực và phép toán trên chúng. ( Phiên bản tiêu chuẩn chạy tới hơn năm mươi trang tài liệu và có một ủy ban để xử lý lỗi sai và sàng lọc thêm.)
Xấp xỉ này là một hỗn hợp các xấp xỉ của các loại khác nhau, mỗi loại có thể bị bỏ qua hoặc tính toán cẩn thận do cách sai lệch cụ thể của nó so với độ chính xác. Nó cũng liên quan đến một số trường hợp đặc biệt 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 ngay trong khi giả vờ không chú ý.
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), bạn nên viết hoặc sử dụng chương trình toán học tượng trưng thay thế.
Nhưng nếu bạn ổn với ý tưởng rằng đôi khi toán học dấu phẩy động mờ về giá trị và logic và các 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à bài kiểm tra của mình để cho phép điều đó, thì mã của bạn có thể thường xuyên nhận được bằng những gì trong FPU của bạn.
Để cho vui, tôi đã chơi với đại diện của phao, theo các định nghĩa từ Tiêu chuẩn C99 và tôi đã viết mã dưới đây.
Mã này in đại diện nhị phân của phao trong 3 nhóm riêng biệt
SIGN EXPONENT FRACTION
và sau đó nó in ra một tổng, rằng, khi được tổng hợp 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 bit được in bởi hàm xx
sao cho tổng được in bởi hàmyy
bằng với số đã cho.
Trong thực tế, số tiền này chỉ là một xấp xỉ. Đố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 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 thuật ngữ 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à phiên giao diện điều khiển trong đó tôi tính giá trị thực của số float tồn tại trong phần cứng. Tôi đã sử dụng bc
để in tổng số 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 đó vào 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ế
999999999.999999446351872
Bạn cũng có thể kiểm tra với bc
-3,14 cũng bị nhiễu loạn. Đừng quên thiết lập một scale
yếu tố bc
.
Tổng số hiển thị là những gì bên trong phần cứng. Giá trị bạn có được bằng cách tính toán nó phụ thuộc vào thang đo bạn đặt. Tôi đã đặt scale
hệ số thành 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 thị 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 trình bày 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 định nghĩa một mã hóa để sử dụng 64 bit này một cách hiệu quả cho không gian số lớn hơn nhiều cộng với NaN và +/- Infinity, do đó, có những khoảng cách giữa các số được biểu diễn chính xác số chỉ xấp xỉ.
Thật không may 0,3 ngồi trong một khoảng cách.
Hãy tưởng tượng làm việc trong cơ sở mười với, nói, 8 chữ số chính xác. Bạn kiểm tra xem
1/3 + 2 / 3 == 1
và học được rằng điều này trở lại false
. Tại sao? Vâng, như những con số thực chúng ta có
1/3 = 0,333 .... và 2/3 = 0,666 ....
Cắt ngắn ở tám chữ số thập phân, chúng tôi nhận được
0.33333333 + 0.66666666 = 0.99999999
đó là, tất nhiên, khác với 1.00000000
chính xác 0.00000001
.
Tình huống cho các số nhị phân có số bit cố định là hoàn toàn tương tự. Là số thực, chúng ta 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 chúng thành bảy bit, thì chúng ta sẽ nhận được
0.0001100 + 0.0011001 = 0.0100101
mặt khác,
3/10 = 0,01001100110011 ... (cơ sở 2)
trong đó, bị cắt ngắn thành bảy bit 0.0100110
, và chúng khác nhau chính xác 0.0000001
.
Tình huống chính xác là tinh tế hơn một chút 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ữ dưới dạng như1.10011 * 2^-4
, tùy thuộc vào số lượng bit chúng ta đã phân bổ cho số mũ và lớp phủ. Điều này ảnh hưởng đến số lượng chính xác bạn nhận được cho các tính toán của mình.
Kết quả cuối cùng là do những lỗi làm tròn này mà về cơ bản bạn không bao giờ muốn sử dụng == trên các số có dấu phẩy động. Thay vào đó, bạn có thể kiểm tra xem giá trị tuyệt đối của chênh lệch của chúng có nhỏ hơn một số số nhỏ cố định hay không.
Vì Python 3.5, bạn có thể sử dụng math.isclose()
hàm để kiểm tra đẳng thức gần đúng:
>>> import math
>>> math.isclose(0.1 + 0.2, 0.3)
True
>>> 0.1 + 0.2 == 0.3
False
Vì chủ đề này đã phân nhánh một chút vào một cuộc thảo luận chung về việc triển khai điểm nổi hiện tại, tôi nói thêm rằng có các dự án khắc phục các vấn đề của họ.
Ví dụ, hãy xem https://poseithub.org/ , trong đó hiển thị một loại số được gọi là posit (và unum tiền thân của nó) hứa hẹn sẽ 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à chính xác, nó cũng sửa chữa các loại vấn đề trong câu hỏi. Dự án khá thú vị, người đứng đằng sau nó là một nhà toán học, Tiến sĩ John Gustafson . Toàn bộ điều này là nguồn mở, với nhiều triển khai thực tế trong C / C ++, Python, Julia và C # ( https://hastlayer.com/arithologists ).
Nó thực sự khá đơn giản. Khi bạn có một hệ thống 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 hệ 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ì tất cả 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à các số thập phân lặp lại vì mẫu số của chúng sử dụng hệ số nguyên tố là 3 hoặc 7. Trong nhị phân (hoặc cơ sở 2), hệ 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 là một yếu tố chính. Trong nhị phân, 1/2, 1/4, 1/8 tất cả sẽ được thể hiện 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 số thập phân. Vì vậy, 0,1 và 0,2 (1/10 và 1/5) trong khi làm sạch số thập phân trong hệ thống cơ sở 10, đang lặp lại số thập phân trong hệ thống cơ sở 2 mà máy tính đang hoạt động. Khi bạn thực hiện toán trên các số thập phân lặp lại này,
Các số thập phâ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ác xấp xỉ cho 0.1
và 0.2
khác với xấp xỉ được sử dụng cho 0.3
, do đó 0.1 + 0.2 == 0.3
có thể thấy rõ hơn sai ở đâ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 tính toán này được đánh giá đáng tin cậy hơn, bạn sẽ cần sử dụng 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 ).
Math.sum (javascript) .... loại thay thế toán tử
.1 + .0001 + -.1 --> 0.00010000000000000286
Math.sum(.1 , .0001, -.1) --> 0.0001
Object.defineProperties(Math, {
sign: {
value: function (x) {
return x ? x < 0 ? -1 : 1 : 0;
}
},
precision: {
value: function (value, precision, type) {
var v = parseFloat(value),
p = Math.max(precision, 0) || 0,
t = type || 'round';
return (Math[t](v * Math.pow(10, p)) / Math.pow(10, p)).toFixed(p);
}
},
scientific_to_num: { // this is from https://gist.github.com/jiggzson
value: function (num) {
//if the number is in scientific notation remove it
if (/e/i.test(num)) {
var zero = '0',
parts = String(num).toLowerCase().split('e'), //split into coeff and exponent
e = parts.pop(), //store the exponential part
l = Math.abs(e), //get the number of zeros
sign = e / l,
coeff_array = parts[0].split('.');
if (sign === -1) {
num = zero + '.' + new Array(l).join(zero) + coeff_array.join('');
} else {
var dec = coeff_array[1];
if (dec)
l = l - dec.length;
num = coeff_array.join('') + new Array(l + 1).join(zero);
}
}
return num;
}
}
get_precision: {
value: function (number) {
var arr = Math.scientific_to_num((number + "")).split(".");
return arr[1] ? arr[1].length : 0;
}
},
sum: {
value: function () {
var prec = 0, sum = 0;
for (var i = 0; i < arguments.length; i++) {
prec = this.max(prec, this.get_precision(arguments[i]));
sum += +arguments[i]; // force float to convert strings to number
}
return Math.precision(sum, prec);
}
}
});
ý tưởng là sử dụng toán tử thay vì toán tử để tránh lỗi float
Math.sum tự động phát hiện độ chính xác để sử dụng
Math.sum chấp nhận bất kỳ số lượng đối số
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õ một điểm dừng 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 dạng nhị phân có độ chính xác kép: định dạng dấu phẩy nhị phân chính xác kép của IEEE 754: binary64
Từ trang Wikipedia cho định dạng dấu phẩy động chính xác kép :
Điểm nổi 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 điểm nổi chính xác đơn, mặc dù hiệu suất và chi phí băng thông của nó. Như với định dạng dấu phẩy động chính xác đơn, nó thiếu độ chính xác trên các số nguyên khi so sánh với một đị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 nhị phân64 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 một mốc thời gian chính xác kép 64 bit nhất định với số mũ sai lệch nhất định và phân số 52 bit là
hoặc là
Cảm ơn @a_guest đã chỉ ra điều đó cho tôi.
Một câu hỏi khác đã được đặt tên là trùng lặp với câu hỏi này:
Trong C ++, tại sao kết quả cout << x
khác với giá trị mà trình gỡ lỗi đang hiển thị x
?
Trong x
câu hỏi là một float
biến.
Một ví dụ sẽ là
float x = 9.9F;
Trình gỡ lỗi cho thấy 9.89999962
, đầu ra của cout
hoạt động là 9.9
.
Câu trả lời hóa ra là cout
độ chính xác mặc định của float
nó là 6, do đó, nó làm tròn đến 6 chữ số thập phân.
Xem tại đây để tham khảo