Là toán nổi điểm bị hỏng?


2983

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?


127
Các biến dấu phẩy động thường có hành vi này. Nó được gây ra bởi cách chúng được lưu trữ trong phần cứng. Để biết thêm thông tin, hãy xem bài viết Wikipedia về số dấu phẩy động .
Ben S

62
JavaScript coi số thập phân là số dấu phẩy động , có nghĩa là các hoạt động như phép cộng có thể bị lỗi làm tròn. Bạn có thể muốn xem bài viết này: Đ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
matt b

4
Chỉ cần thông tin, TẤT CẢ các loại số trong javascript là IEEE-754 Nhân đôi.
Gary Willoughby

6
Vì JavaScript sử dụng tiêu chuẩn IEEE 754 cho Toán học, nên nó sử dụng các số trôi nổi 64 bit . Điều này gây ra lỗi chính xác khi thực hiện các phép tính dấu phẩy động (thập phân), trong ngắn hạn, do các máy tính làm việc trong Cơ sở 2 trong khi số thập phân là Cơ sở 10 .
Pardeep Jain

Câu trả lời:


2252

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.1tiê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ặc
  • 0x1.999999999999ap-4trong ký hiệu hexfloat C99 .

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

  • 0.1 ở dạng thập phân, hoặc
  • 0x1.99999999999999...p-4trong một 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.20.3trong 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.2nhưng điều đó gần nhất doubleđể 0.3là nhỏ hơn so với số lượng hợp lý 0.3. Tổng 0.10.2kết thúc là lớn hơn số hữu tỷ 0.3và do đó không đồng ý với hằng số trong mã của bạn.

Một cách xử lý khá toàn diện về các vấn đề số học dấu phẩy động là điều mà 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 abslà giá trị tuyệt đối. myToleranceValuecầ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.


181
Tôi nghĩ rằng "một số lỗi không đổi" là chính xác hơn "Epsilon" bởi vì không có "The Epsilon" có thể được sử dụng trong mọi trường hợp. Các epsilon khác nhau cần được sử dụng trong các tình huống khác nhau. Và máy epsilon gần như không bao giờ là một hằng số tốt để sử dụng.
Rotsor

34
Điều đó không hoàn toàn đúng khi tất cả các phép toán dấu phẩy động đều dựa trên tiêu chuẩn IEEE [754]. Chẳng hạn, vẫn còn một số hệ thống được sử dụng có hệ thập lục phân cũ của IBM, và vẫn còn các card đồ họa không hỗ trợ số học của IEEE-754. Tuy nhiên, điều đó đúng với một xấp xỉ hợp lý.
Stephen Canon

19
Cray đã bỏ qua việc tuân thủ chuẩn IEEE-754. Java cũng nới lỏng sự tuân thủ của nó như là một tối ưu hóa.
Nghệ thuật Taylor

28
Tôi nghĩ bạn nên thêm một cái gì đó vào câu trả lời này về cách tính toán tiền luôn luôn, luôn luôn được thực hiện với số học điểm cố định trên các số nguyên , bởi vì tiền được lượng tử hóa. (Có thể có ý nghĩa khi thực hiện các tính toán kế toán nội bộ theo phân số nhỏ của một xu, hoặc bất kể đơn vị tiền tệ nhỏ nhất của bạn là gì - điều này thường giúp giảm ví dụ khi chuyển "29,99 đô la một tháng" sang tỷ lệ hàng ngày - nhưng nên vẫn là số học điểm cố định.)
zwol

18
Thực tế thú vị: chính 0,1 không được thể hiện chính xác trong dấu phẩy động nhị phân này đã gây ra lỗi phần mềm tên lửa Patriot khét tiếng khiến 28 người thiệt mạng trong cuộc chiến tranh Irac đầu tiên.
hdl

602

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 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.

1. Sơ lượt

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.

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 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.

3. Nguyên nhân của lỗi làm tròn số 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). 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= 4cá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.

4. 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 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.

5. Hoạt động lặp đi 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.

6. Tóm tắt

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.


8
(3) là sai. Lỗi làm tròn trong một bộ phận không ít hơn một đơn vị ở vị trí cuối cùng, nhưng nhiều nhất là một nửa đơn vị ở vị trí cuối cùng.
gnasher729

6
@ gnasher729 Bắt tốt. Hầu hết các hoạt động cơ bản cũng có lỗi nhỏ hơn 1/2 của một đơn vị ở vị trí cuối cùng sử dụng chế độ làm tròn mặc định của IEEE. Đã chỉnh sửa lời giải thích và cũng lưu ý rằng lỗi có thể lớn hơn 1/2 của một ulp nhưng nhỏ hơn 1 ulp nếu người dùng ghi đè chế độ làm tròn mặc định (điều này đặc biệt đúng trong các hệ thống nhúng).
KernelPanik

39
(1) Số dấu phẩy động không có lỗi. Mỗi giá trị dấu phẩy động là chính xác những gì nó là. Hầu hết (nhưng không phải tất cả) các phép toán dấu phẩy động cho kết quả không chính xác. Ví dụ: không có giá trị dấu phẩy động nhị phân chính xác bằng 1.0 / 10.0. Một số thao tác (ví dụ: 1.0 + 1.0) mặt khác cho kết quả chính xác.
Solomon chậm

19
"Nguyên nhân chính gây ra 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ố" là một điều rất dễ gây hiểu lầm. Đối với phân chia tuân thủ theo chuẩn IEEE-754, nguyên nhân duy nhất gây ra lỗi trong phân chia dấu phẩy động là sự bất lực của kết quả được thể hiện chính xác trong định dạng kết quả; kết quả tương tự được tính bất kể thuật toán được sử dụng.
Stephen Canon

6
@Matt Xin lỗi vì phản hồi muộn. Về cơ bản là do vấn đề tài nguyên / thời gian và sự đánh đổi. Có một cách để phân chia dài / phân chia 'bình thường' hơn, đó gọi là Phân chia SRT với cơ số hai. Tuy nhiên, điều này liên tục thay đổi và trừ đi số chia khỏi cổ tức và mất nhiều chu kỳ đồng hồ vì nó chỉ tính một bit của thương số trên mỗi chu kỳ đồng hồ. Chúng tôi sử dụng các bảng đối ứng để có thể tính toán nhiều bit hơn của thương số trên mỗi chu kỳ và thực hiện đánh đổi hiệu suất / tốc độ hiệu quả.
KernelPanik

462

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.


133
Câu trả lời tuyệt vời và ngắn gọn. Mẫu lặp lại trông giống như 0,00011001100110011001100110011001100110011001100110011 ...
Konstantin Chernov

4
Điều này không giải thích tại sao không phải là một thuật toán tốt hơn được sử dụng mà không chuyển đổi thành nhị phân ở vị trí đầu tiên.
Dmitri Zaitsev

12
Vì hiệu suất. Sử dụng nhị phân nhanh hơn vài nghìn lần, vì đó là nguồn gốc của máy.
Joel Coehoorn

7
Có các phương pháp mang lại giá trị thập phân chính xác. BCD (số thập phân được mã hóa nhị phân) hoặc các dạng số thập phân khác. Tuy nhiên, cả hai đều chậm hơn (chậm hơn rất nhiều) và chiếm nhiều dung lượng hơn so với sử dụng dấu phẩy động nhị phân. (ví dụ: BCD được đóng gói lưu trữ 2 chữ số thập phân trong một byte. Đó là 100 giá trị có thể trong một byte thực sự có thể lưu trữ 256 giá trị có thể, hoặc 100/256, làm lãng phí khoảng 60% giá trị có thể của một byte.)
Duncan C

16
@Jacksonkr bạn vẫn đang suy nghĩ ở cơ sở 10. Máy tính là cơ sở-2.
Joel Coehoorn

306

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.

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


3
Lưu ý rằng có một số ngôn ngữ bao gồm toán chính xác. Một ví dụ là Scheme, ví dụ thông qua GNU Guile. Xem Drainketo.de/english/exact-math-to-the-resTHER - những thứ này giữ cho toán học dưới dạng phân số và cuối cùng chỉ cắt ra.
Arne Babenhauserheide

5
@FloatingRock Trên thực tế, rất ít ngôn ngữ lập trình chính thống có số hữu tỷ tích hợp. Arne là một Schemer, như tôi, vì vậy đây là những thứ chúng ta được chiều chuộng.
Chris Jester-Young

5
@ArneBabenhauserheide Tôi nghĩ rằng đáng để thêm rằng điều này sẽ chỉ hoạt động với các số hữu tỷ. Vì vậy, nếu bạn đang làm một số phép toán với các số vô tỷ như pi, bạn sẽ phải lưu nó dưới dạng bội số của pi. Tất nhiên, mọi phép tính liên quan đến số pi không thể được biểu diễn dưới dạng số thập phân chính xác.
Aidiakapi 11/03/2015

13
@connexo Được rồi. Làm thế nào bạn sẽ lập trình quay bánh pizza của bạn để có được 36 độ? 36 độ là gì? (Gợi ý: nếu bạn có thể xác định điều này theo cách chính xác, bạn cũng có một máy cắt pizza-lát-an-chính xác-thứ mười.) Nói cách khác, bạn thực sự không thể có 1/360 (một độ) hoặc 1 / 10 (36 độ) chỉ với điểm nổi nhị phân.
Chris Jester-Young

12
@connexo Ngoài ra, "mỗi thằng ngốc" không thể xoay một chiếc bánh pizza chính xác 36 độ. Con người quá dễ mắc lỗi để làm bất cứ điều gì khá chính xác.
Chris Jester-Young

212

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.


133
máy tính không cần một lượng bộ nhớ vô hạn để có được 0,1 + 0,2 = 0,3
Pacerier

23
@Pacerier Chắc chắn, họ có thể sử dụng hai số nguyên có độ chính xác không giới hạn để biểu diễn một phân số hoặc họ có thể sử dụng ký hiệu trích dẫn. Đó là khái niệm cụ thể về "nhị phân" hoặc "thập phân" làm cho điều này không thể thực hiện được - ý tưởng rằng bạn có một chuỗi các chữ số nhị phân / thập phân và, ở đâu đó trong đó, một điểm cơ số. Để có kết quả hợp lý chính xác, chúng tôi cần một định dạng tốt hơn.
Devin Jeanpierre

15
@Pacerier: Không phải dấu phẩy động nhị phân hay dấu thập phân chính xác có thể lưu trữ chính xác 1/3 hoặc 1/13. Các loại dấu phẩy động thập phân có thể biểu diễn chính xác các giá trị của mẫu M / 10 ^ E, nhưng kém chính xác hơn các số dấu phẩy động nhị phân có kích thước tương tự khi biểu thị hầu hết các phân số khác . Trong nhiều ứng dụng, sẽ có độ chính xác cao hơn với các phân số tùy ý hơn là có độ chính xác hoàn hảo với một vài "đặc biệt".
supercat

13
@Pacerier Họ làm nếu họ lưu trữ các số dưới dạng số nhị phân, đó là điểm của câu trả lời.
Mark Amery

3
@chux: Sự khác biệt về độ chính xác giữa các loại nhị phân và thập phân không lớn, nhưng chênh lệch 10: 1 trong trường hợp tốt nhất so với độ chính xác trong trường hợp xấu nhất đối với các loại thập phân lớn hơn nhiều so với chênh lệch 2: 1 với các loại nhị phân. Tôi tò mò liệu có ai đã xây dựng phần cứng hoặc phần mềm bằng văn bản để hoạt động hiệu quả trên một trong hai loại thập phân hay không, vì dường như không thể thực hiện hiệu quả trong phần cứng cũng như phần mềm.
supercat

121

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.3trả về falsetrong 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: 2550xu 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) .


3
Vấn đề là bản thân việc chuyển đổi là không chính xác. 16,08 * 100 = 1607.9999999999998. Chúng ta có phải dùng đến cách chia số và chuyển đổi riêng (như trong 16 * 100 + 08 = 1608) không?
Jason

38
Giải pháp ở đây là thực hiện tất cả các tính toán của bạn theo số nguyên sau đó chia cho tỷ lệ của bạn (100 trong trường hợp này) và chỉ làm tròn khi trình bày dữ liệu. Điều đó sẽ đảm bảo rằng tính toán của bạn sẽ luôn chính xác.
David Granado

15
Chỉ cần nhấn mạnh một chút: số học số nguyên chỉ chính xác trong dấu phẩy động đến một điểm (ý định chơi chữ). Nếu số này lớn hơn 0x1p53 (để sử dụng ký hiệu dấu phẩy động thập lục phân của Java 7, = 9007199254740992), thì ulp là 2 tại điểm đó và do đó 0x1p53 + 1 được làm tròn xuống 0x1p53 (và 0x1p53 + 3 được làm tròn xuống 0x1p53 4, vì tròn đến chẵn). :-D Nhưng chắc chắn, nếu số của bạn nhỏ hơn 9 triệu, bạn sẽ ổn thôi. :-P
Chris Jester-Young

2
Jason, bạn chỉ nên làm tròn kết quả (int) (16,08 * 100 + 0,5)
Mikhail Semenov

@CodyBugstein " Vậy làm thế nào để bạn có được .1 + .2 để hiển thị .3? " Viết hàm in tùy chỉnh để đặt số thập phân ở nơi bạn muốn.
RonJohn 15/03/19

113

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:

  • Bit đầu tiên là bit dấu : 1nếu số âm, 0nếu không thì 1 .
  • 11 bit tiếp theo là số mũ , được 1023. Nói cách khác, sau khi đọc các bit số mũ từ một số chính xác kép, 1023 phải được trừ để có được công suất của hai.
  • 52 bit còn lại là có ý nghĩa (hoặc mantissa). Trong lớp phủ, một 'hàm ý' 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-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ộ 1s) 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ị ab , 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 ab chỉ khác nhau ở bit cuối cùng; ...0011+ 1= ...0100. Trong trường hợp này, giá trị có bit có ý nghĩa nhỏ nhất bằng 0 là b , vì vậy tổng là:

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

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

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

chỉ khác với biểu diễn nhị phân của tổng 0,1 và 0,2 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.2mộ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.2cung cấp 0.3: một vài bit cuối cùng được làm tròn.


14
Câu trả lời của tôi đã được bỏ phiếu ngay sau khi đăng nó. Kể từ đó, tôi đã thực hiện nhiều thay đổi (bao gồm ghi chú rõ ràng các bit định kỳ khi viết 0,1 và 0,2 ở dạng nhị phân, mà tôi đã bỏ qua trong bản gốc). Nếu bạn bỏ phiếu thấy điều này, bạn có thể vui lòng cho tôi một số phản hồi để tôi có thể cải thiện câu trả lời của mình không? Tôi cảm thấy rằng câu trả lời của tôi bổ sung thêm một điều mới vì cách xử lý tổng trong IEEE 754 không được đề cập theo cách tương tự trong các câu trả lời khác. Trong khi "Điều mà mọi nhà khoa học máy tính nên biết ..." bao gồm một số tài liệu tương tự, câu trả lời của tôi đề cập cụ thể đến trường hợp 0,1 + 0,2.
Ái Hà Lee

57

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.1sẽ 1 x 10⁻¹, 0.2sẽ 2 x 10⁻¹0.3sẽ 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.2rõ 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.51 x 2⁻¹0.251 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.13602879701896397 x 2⁻⁵⁵và số gần nhất 0.27205759403792794 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ị.


2
@Mark Cảm ơn bạn đã giải thích rõ ràng này nhưng sau đó câu hỏi đặt ra tại sao 0,1 + 0,4 chính xác thêm tới 0,5 (ít nhất trong Python 3). Ngoài ra, cách tốt nhất để kiểm tra sự bình đẳng khi sử dụng float trong Python 3 là gì?
pchegoor

2
@ user2417881 Các phép toán dấu phẩy động của IEEE có các quy tắc làm tròn cho mọi thao tác và đôi khi việc làm tròn có thể tạo ra một câu trả lời chính xác ngay cả khi hai số bị tắt một chút. Các chi tiết quá dài cho một nhận xét và dù sao tôi cũng không phải là chuyên gia. Như bạn thấy trong câu trả lời này 0,5 là một trong số ít số thập phân có thể được biểu diễn dưới dạng nhị phân, nhưng đó chỉ là sự trùng hợp ngẫu nhiên. Để kiểm tra tính bằng, hãy xem stackoverflow.com/questions/5595425/ .
Đánh dấu tiền chuộc

1
@ user2417881 câu hỏi của bạn khiến tôi tò mò vì vậy tôi đã biến nó thành một câu hỏi và câu trả lời đầy đủ: stackoverflow.com/q/48374522/5987
Đánh dấu tiền chuộc

47

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.


33

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 quá trình cộng.


30

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ư

nhập mô tả hình ảnh ở đây

Mặc dù bạn có thể gõ 0.2dễ dàng, FLT_RADIXDBL_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.


28

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%).


28

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

Thật không may, số học dấu phẩy động chính xác, thật không may, nó không khớp với cách biểu diễn số 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 đó.


Làm tròn đến số nguyên gần nhất không phải là cách an toàn để giải quyết vấn đề so sánh trong mọi trường hợp. 0.4999998 và 0.500001 làm tròn đến các số nguyên khác nhau, do đó, có "vùng nguy hiểm" xung quanh mọi điểm cắt tròn. (Tôi biết các chuỗi thập phân đó có thể không thể biểu diễn chính xác như các float nhị phân của IEEE.)
Peter Cordes

1
Ngoài ra, mặc dù dấu phẩy động là định dạng "di sản", nó được thiết kế rất tốt. Tôi không biết bất cứ điều gì mà bất cứ ai sẽ thay đổi nếu thiết kế lại bây giờ. Càng tìm hiểu về nó, tôi càng nghĩ nó được thiết kế rất tốt . ví dụ: số mũ thiên vị có nghĩa là số float nhị phân liên tiếp có các biểu diễn số nguyên liên tiếp, do đó bạn có thể thực hiện 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).
Peter Cordes

Tôi không đồng ý, các phao nên được lưu trữ dưới dạng số thập phân và không phải là nhị phân và tất cả các vấn đề được giải quyết.
Ronen Festinger

Không nên " x / (2 ^ n + 5 ^ n) " là " x / (2 ^ n * 5 ^ n) "?
Wai Ha Lee

@RonenFestinger - còn 1/3 thì sao?
Stephen C

19

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 **


19

Để đư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.20.7đây là : 0.2 + 0.7 = 0.8999999999999999.

Kết quả mong đợi của bạn là 0.9nó có nghĩa là bạn cần một kết quả 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 / 10sẽ 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.


điều này làm tôi đau đầu thực sự. Tôi tính tổng 12 số float, sau đó hiển thị tổng và trung bình nếu các số đó. sử dụng toFixed () có thể sửa tổng 2 số, nhưng khi tính tổng vài số thì bước nhảy là rất đáng kể.
Nuryagdy Mustapayev

@Nuryagdy Mustapayev Tôi không có ý định của bạn, vì tôi đã kiểm tra trước khi bạn có thể tính tổng 12 số float, sau đó sử dụng hàm floatify () trên kết quả, sau đó làm bất cứ điều gì bạn muốn trên đó, tôi quan sát thấy không có vấn đề gì khi sử dụng nó.
Mohammad Musavi

Tôi chỉ nói trong tình huống của tôi khi tôi có khoảng 20 tham số và 20 công thức trong đó kết quả của mỗi công thức phụ thuộc vào người khác, giải pháp này không giúp được gì.
Nuryagdy Mustapayev

16

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).


Tôi không hiểu đoạn thứ hai của bạn cả.
Nae

1
@Nae Tôi sẽ dịch đoạn thứ hai là "Phần lớn các phân số không thể được biểu diễn chính xác bằng số thập phân hoặc nhị phân. Vì vậy, hầu hết các kết quả sẽ được làm tròn - mặc dù chúng vẫn chính xác với số bit / chữ số vốn có trong biểu diễn đang được sử dụng."
Steve Hội nghị thượng đỉnh

15

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 floathoặc doublesang BigDecimalJava.

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.


2
Đối với người chỉnh sửa tôi vừa quay lại: Tôi xem xét các trích dẫn mã phù hợp để trích dẫn mã. Câu trả lời này, trung lập với ngôn ngữ, hoàn toàn không chứa bất kỳ mã trích dẫn nào. Số có thể được sử dụng trong câu tiếng Anh và điều đó không biến chúng thành mã.
Patricia Shanahan

Đây có thể là lý do tại sao ai đó định dạng số của bạn dưới dạng mã - không phải để định dạng, mà là để dễ đọc.
Wai Ha Lee

... cũng có, các vòng để thậm chí đề cập đến nhị phân đại diện, không những thập phân đại diện. Xem cái này hoặc, ví dụ, cái này .
Wai Ha Lee

@WaiHaLee Tôi không áp dụng thử nghiệm lẻ / chẵn cho bất kỳ số thập phân nào, chỉ có thập lục phân. Một chữ số thập lục phân là ngay cả khi, và chỉ khi, bit quan trọng nhất của khai triển nhị phân của nó bằng không.
Patricia Shanahan

14

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ụ:

  • decimalMô-đ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
    

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

  • fractionsMô-đun của Python và BigFractionlớ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.


14

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)=truetrừ 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/10cố 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 ...


Vì con người sử dụng số thập phân, tôi thấy không có lý do chính đáng tại sao phao không được biểu thị dưới dạng thập phân theo mặc định để chúng tôi có kết quả chính xác.
Ronen Festinger

Con người sử dụng nhiều cơ sở khác ngoài cơ sở 10 (số thập phân), nhị phân là cơ sở chúng ta sử dụng nhiều nhất cho điện toán .. 'lý do chính đáng' là bạn đơn giản không thể đại diện cho mọi phân số trong mọi cơ sở ..

Số học nhị phân @RonenFestinger rất dễ thực hiện trên máy tính vì chỉ cần tám thao tác cơ bản với các chữ số: giả sử $ a $, $ b $ bằng $ 0,1 $ tất cả những gì bạn cần biết là $ \ operatorname {xor} (a, b) $ và $ \ operatorname {cb} (a, b) $, trong đó xor là độc quyền hoặc và cb là "bit carry" là $ 0 $ trong mọi trường hợp trừ khi $ a = 1 = b $, trong trường hợp đó chúng ta có một (trên thực tế giao hoán của tất cả các hoạt động giúp bạn tiết kiệm được 2 đô la trường hợp và tất cả những gì bạn cần là quy tắc 6 đô la). Mở rộng thập phân cần các trường hợp $ 10 \ lần 11 $ (ký hiệu thập phân) để được lưu trữ và $ 10 $ các trạng thái khác nhau cho mỗi bit và lãng phí lưu trữ khi mang theo.
Oskar Limka

@RonenFestinger - Số thập phân KHÔNG chính xác hơn. Đó là những gì câu trả lời này đang nói. Đối với bất kỳ cơ sở nào bạn đã chọn, sẽ có các số (phân số) hợp lý cung cấp một chuỗi số lặp lại vô hạn. Đối với hồ sơ, một số máy tính đầu tiên đã sử dụng các biểu diễn cơ sở 10 cho các số, nhưng các nhà thiết kế phần cứng máy tính tiên phong đã sớm kết luận rằng cơ sở 2 dễ thực hiện và hiệu quả hơn nhiều.
Stephen C

9

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.


9

Để 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 xxsao 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 replhoặc một cái gì đó tương tự cũng có.

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

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

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 scaleyế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 scalehệ 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.


5

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.


4

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 ....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.00000000chí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)

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.0001100chú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.


4

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

3

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 ).


3

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,

Từ https://0.30000000000000004.com/


3

Các số thập phân như 0.1, 0.20.3không được biểu diễn chính xác trong các loại dấu phẩy động được mã hóa nhị phân. Tổng các xấp xỉ cho 0.10.2khác với xấp xỉ được sử dụng cho 0.3, do đó 0.1 + 0.2 == 0.3có 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_Decimal128loại có thể có sẵn trên hệ thống của bạn (ví dụ, GCC hỗ trợ họ về các mục tiêu được lựa chọn , nhưng Clang không hỗ trợ họ trên OS X ).


1

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ố


1
Tôi không chắc chắn bạn đã trả lời câu hỏi, " Tại sao những điều không chính xác này xảy ra? "
Ái Hà Lee

theo cách bạn đúng nhưng tôi đến đây từ một hành vi lạ của javascript liên quan đến vấn đề này ... tôi chỉ muốn chia sẻ loại giải pháp
bortunac

Bạn vẫn không trả lời câu hỏi mặc dù.
Lee

k bạn có vấn đề với điều này ... hãy cho tôi biết nơi để di chuyển nó hoặc nếu bạn khăng khăng tôi có thể xóa nó đi
bortunac

0

Tôi chỉ 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õ 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

Nhập mô tả hình ảnh ở đây

Đ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)

Nhập mô tả hình ảnh ở đây

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à

Nhập mô tả hình ảnh ở đây

hoặc là

Nhập mô tả hình ảnh ở đây

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


-1

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 << xkhác với giá trị mà trình gỡ lỗi đang hiển thị x?

Trong xcâu hỏi là một floatbiế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 couthoạt động là 9.9.

Câu trả lời hóa ra là coutđộ chính xác mặc định của floatnó là 6, do đó, nó làm tròn đến 6 chữ số thập phân.

Xem tại đây để tham khảo


1
IMO - đăng bài này ở đây là cách tiếp cận sai. Tôi biết điều đó thật khó chịu, nhưng những người cần câu trả lời cho câu hỏi ban đầu (dường như đã bị xóa!) Sẽ không tìm thấy nó ở đây. Nếu bạn thực sự cảm thấy rằng công việc của bạn xứng đáng được lưu lại, tôi sẽ đề nghị: 1) tìm kiếm một Q khác mà điều này thực sự trả lời, 2) tạo ra một câu hỏi tự trả lời.
Stephen C
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.