Câu hỏi Toán học dấu chấm động có bị hỏng không?


Xem xét mã sau:

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

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


2268
2018-02-25 21:39


gốc


Các biến số dấu chấm độ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 trên Wikipedia về số dấu phẩy động. - Ben S
JavaScript xử lý số thập phân như số điểm nổi, có nghĩa là các hoạt động như bổ sung có thể bị lỗi làm tròn. Bạn có thể muốn xem bài viết này: Mỗi nhà khoa học máy tính nên biết gì về số học dấu chấm động - matt b
Chỉ cần cho thông tin, TẤT CẢ các loại số trong javascript là IEEE-754 tăng gấp đôi. - Gary Willoughby
@Gary True, mặc dù bạn được đảm bảo có độ chính xác số nguyên hoàn hảo cho các số nguyên tối đa 15 chữ số, hãy xem hunlock.com/blogs/The_Complete_Javascript_Number_Reference - Ender
Bởi vì JavaScript sử dụng chuẩn IEEE 754 cho Toán, nó sử dụng 64 bit số nổi. Đ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 chấm độ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 thập phân là Cơ sở 10. - Pardeep Jain


Các câu trả lời:


Nhị phân điểm nổi toán học là như thế này. Trong hầu hết các ngôn ngữ lập trình, nó dựa trên Chuẩn IEEE 754. JavaScript sử dụng biểu diễn dấu phẩy động 64-bit, giống như của Java double. Điểm mấu chốt của vấn đề là các số được biểu diễn ở định dạng này với số nguyên gấp một lần sức mạnh của hai; số hữu tỉ (chẳng hạn như 0.1, đó là 1/10) có mẫu số không phải là sức mạnh của hai mẫu không thể được biểu diễn chính xác.

Dành cho 0.1 trong tiêu chuẩn binary64 định dạng, biểu diễn có thể được viết chính xác như

  • 0.1000000000000000055511151231257827021181583404541015625 theo thập phân, hoặc
  • 0x1.999999999999ap-4 trong Ký hiệu hexadlo C99.

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

  • 0.1 theo thập phân, hoặc
  • 0x1.99999999999999...p-4 trong một tương tự của ký hiệu hexadlo C99, trong đó ... đại diện cho chuỗi 9 bất tận.

Các hằng số 0.2 và 0.3 trong chương trình của bạn cũng sẽ xấp xỉ giá trị thực của chúng. Nó xảy ra gần nhất double đến 0.2 lớn hơn số hữu tỷ 0.2 nhưng cái gần nhất double đến 0.3 nhỏ hơn số hữu tỷ 0.3. Tổng của 0.1 và 0.2 gió lên 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 điều trị khá toàn diện về các vấn đề số học dấu chấm động là Mỗi nhà khoa học máy tính nên biết gì về số học dấu chấm động. Để có giải thích dễ hiểu hơn, hãy xem floating-point-gui.de.


1723
2018-04-18 11:52



'Một số hằng số lỗi' còn được gọi là giá trị Epsilon. - Gary Willoughby
Tôi nghĩ rằng "một số lỗi liên tục" là chính xác hơn "The Epsilon" bởi vì không có "The Epsilon" có thể được sử dụng trong mọi trường hợp. Epsilons khác nhau cần phải được sử dụng trong các tình huống khác nhau. Và máy epsilon hầu như không bao giờ là một hằng số tốt để sử dụng. - Rotsor
Nó không phải khá đúng là tất cả toán học dấu chấm động dựa trên chuẩn IEEE [754]. Vẫn còn một số hệ thống đang sử dụng có bộ vi xử lý thập lục phân IBM cũ, ví dụ, và vẫn còn các cạc đồ họa không hỗ trợ số học IEEE-754. Tuy nhiên, nó đúng với một xấp xỉ hợp lý. - Stephen Canon
Cray đã bỏ qua sự tuân thủ của IEEE-754 về tốc độ. Java cũng nới lỏng sự tuân thủ của nó như là một sự tối ưu hóa. - Art Taylor
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 số nguyên, bởi vì tiền được lượng tử hóa. (Có thể có ý nghĩa khi tính toán nội bộ trong các phần nhỏ của một xu hoặc bất kỳ đơn vị tiền tệ nhỏ nhất nào của bạn - điều này thường giúp giảm bớt lỗi khi chuyển đổi "29,99 đô la một tháng" sang tỷ lệ hàng ngày - nhưng vẫn là số học điểm cố định.) - zwol


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

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

1. Khái quát chung

Từ quan điểm kỹ thuật, hầu hết các hoạt động điểm nổi sẽ có một số yếu tố lỗi vì phần cứng thực hiện các tính toán dấu phẩy động chỉ cần có một lỗi nhỏ hơn một nửa của một đơn vị ở vị trí cuối cùng. Do đó, nhiều phần cứng sẽ dừng lại ở độ chính xác chỉ cần thiết để mang lại lỗi dưới một nửa của một đơn vị ở vị trí cuối cùng cho một duy nhất hoạt động đặc biệt có vấn đề trong phân chia điểm nổi. Điều gì cấu thành một phép toán đơn lẻ phụ thuộc vào số lượng toán hạng mà đơn vị nhận được. Đối với hầu hết, nó là hai, nhưng một số đơn vị có 3 hoặc nhiều toán hạng. 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 thêm lên theo thời gian.

2. Tiêu chuẩn

Hầu hết các nhà chế biến đều theo dõi IEEE-754 tiêu chuẩn nhưng một số sử dụng 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 chấm động rất nhỏ với chi phí chính xác. Tuy nhiên, sau đây sẽ bao gồm chế độ chuẩn hóa của IEEE-754 là chế độ hoạt động điển hình.

Trong tiêu chuẩn IEEE-754, các nhà thiết kế phần cứng được cho phép 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 nhỏ hơn một nửa của một đơn vị trong đặt cho một hoạt động. Điều này giải thích lý do tại sao khi có các hoạt động lặp đi lặp lại, các lỗi thêm lên. Đối với độ chính xác kép IEEE-754, đây là bit thứ 54, vì 53 bit được sử dụng để đại diện cho 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 đi vào chi tiết hơn về nguyên nhân của lỗi phần cứng trên các hoạt động điểm nổi khác nhau.

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

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

3.1 Lỗi làm tròn phân chia: xấp xỉ đối ứng

Những sự đối ứng nào trong bảng lựa chọn thương phụ thuộc vào -phương pháp chia: phân chia chậm như phân chia SRT, hoặc phân chia nhanh như bộ phận Goldschmidt; mỗi mục được sửa đổi theo thuật toán phân chia trong một nỗ lực để mang lại lỗi thấp nhất có thể. Trong mọi trường hợp, mặc dù, tất cả các đối ứng đều là xấp xỉ của đối ứng thực tế và giới thiệu một số yếu tố của lỗi. Cả hai phương pháp phân chia nhanh và phân chia nhanh tính toán thương số lặp lại, nghĩa 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ừ từ cổ tức, và dải phân cách lặp lại các bước cho đến khi lỗi nhỏ hơn một nửa đơn vị ở vị trí cuối cùng. Phương pháp phân chia chậm tính toán 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à phương pháp phân chia nhanh tính toán số chữ số trên mỗi bước và thường đắt hơn để 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 lại bởi một xấp xỉ của một nghịch đảo, vì vậy chúng dễ bị lỗi.

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

Một nguyên nhân khác của lỗi làm tròn trong tất cả các thao tác là các chế độ khác nhau của việc cắt ngắn câu trả lời cuối cùng mà IEEE-754 cho phép. Có cắt ngắn, tròn về phía không, vòng tới gần nhất (mặc định), vòng tròn, và tròn lên. Tất cả các phương thức đều đưa ra một phần tử lỗi dưới một đơn vị ở vị trí cuối cùng cho một thao tác đơn lẻ. Theo thời gian và hoạt động lặp lại, cắt ngắn cũng bổ sung thêm một cách tích lũy vào 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ố dạng lặp lại nhân.

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

Vì phần cứng thực hiện các tính toán dấu phẩy động chỉ cần mang lại kết quả với 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 đơn lẻ, lỗi sẽ phát triển trên các thao tác lặp lại nếu không được xem. Đây là lý do mà trong tính toán yêu cầu một lỗi bị giới hạn, các nhà toán học sử dụng các phương pháp như sử dụng vòng tới gần nhất ngay cả chữ số ở nơi 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 hủy bỏ lẫn nhau hơn, và Số học khoảng thời gian 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, vòng tới 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, tròn tới gần nhất ngay cả chữ số ở nơi cuối cùng, đảm bảo lỗi ít hơn một nửa của một đơn vị ở vị trí cuối cùng cho một thao tác. Sử dụng cắt ngắn, làm tròn, và làm tròn một mình có thể dẫn đến 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 nghị trừ khi chúng được sử dụng trong số học Interval.

6. Tóm tắt

Trong ngắn hạn, lý do cơ bản cho các lỗi trong các hoạt động điểm nổi là một sự kết hợp của sự cắt ngắn trong phần cứng, và cắt ngắn một nghịch đảo trong trường hợp phân chia. Vì 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 đơn lẻ, các lỗi dấu phẩy động trên các thao tác lặp lại sẽ tăng lên trừ khi được sửa chữa.


490
2018-02-25 21:43



(3) là sai. Lỗi làm tròn trong một bộ phận không nhỏ hơn một đơn vị ở vị trí cuối cùng, nhưng nhiều nhất một nửa một đơn vị ở vị trí cuối cùng. - gnasher729
@ gnasher729 Tốt bắt. Hầu hết các hoạt động cơ bản cũng có lỗi vi ít hơn 1/2 của một đơn vị ở vị trí cuối cùng bằng cách sử dụng chế độ làm tròn IEEE mặc định. Đã chỉnh sửa 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
(1) Điểm nổi số không có lỗi. Mỗi giá trị dấu phẩy động đều chính xác nó là gì. Điểm nổi bật nhất (nhưng không phải tất cả) hoạt động đưa ra kết quả không chính xác. Ví dụ, không có giá trị dấu chấm động nhị phân nào chính xác bằng 1.0 / 10.0. Một số thao tác (ví dụ: 1.0 + 1.0) làm đưa ra kết quả chính xác mặt khác. - james large
"Nguyên nhân chính của lỗi trong phân chia dấu chấm động, là các thuật toán phân chia được sử dụng để tính toán thương" là một rất điều gây hiểu nhầm để nói. Đối với bộ phận tuân thủ IEEE-754, chỉ có nguyên nhân của lỗi trong phân chia dấu phẩy động là không có khả năng kết quả được biểu diễ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
@Matt Xin lỗi vì đã trả lời muộn. Đó là cơ bản do các vấn đề về tài nguyên / thời gian và sự cân bằng. Có một cách để làm phân chia dài / phân chia 'bình thường' hơn, nó được gọi là SRT Division với cơ số hai. Tuy nhiên, điều này liên tục thay đổi và trừ đi số chia từ cổ tức và mất nhiều chu kỳ đồng hồ vì nó chỉ tính một chút của thương số cho mỗi chu kỳ đồng hồ. Chúng tôi sử dụng các bảng đối ứng để chúng tôi 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 hiệu suất / tốc độ cân bằng. - KernelPanik


Khi bạn chuyển đổi .1 hoặc 1/10 thành cơ số 2 (nhị phân), bạn sẽ nhận được mẫu lặp lại sau dấu thập phân, giống như cố gắng biểu diễn 1/3 trong cơ sở 10. Giá trị không chính xác và do đó bạn không thể thực hiện toán học chính xác với nó bằng cách sử dụng các phương thức dấu phẩy động thông thường.


357
2017-11-20 02:39



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
Đ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 mã nhị phân ở vị trí đầu tiên. - Dmitri Zaitsev
Bởi vì hiệu suất. Sử dụng nhị phân nhanh hơn vài nghìn lần vì nó có nguồn gốc từ máy. - Joel Coehoorn
Có những 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, đây là cả hai chậm hơn (một LOT chậm hơn) và lưu trữ nhiều hơn so với sử dụng điểm nổi nhị phân. (ví dụ, BCD đó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 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 byte). - Duncan C
@ Jacksonkr bạn vẫn đang suy nghĩ trong cơ sở-10. Máy tính là cơ sở-2. - Joel Coehoorn


Hầu hết các câu trả lời ở đây đề cập đến câu hỏi này theo các thuật ngữ kỹ thuật rất khô khan. Tôi muốn giải quyết điều này theo nghĩa 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ên bánh pizza. Bạn có một máy cắt bánh pizza có thể cắt lát bánh pizza chính xác Trong một nửa. Nó có thể giảm một nửa số pizza, hoặc nó có thể giảm một nửa một lát hiện có, nhưng trong mọi trường hợp, việc giảm một nửa luôn chính xác.

Máy cắt bánh pizza có các chuyển động rất tốt, và nếu bạn bắt đầu với một bánh pizza, sau đó giảm một nửa, và tiếp tục giảm một nửa phần nhỏ nhất mỗi lần, bạn có thể làm giảm một nửa 53 lần trước khi lát quá nhỏ, ngay cả 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 phần cắt mỏng đó, nhưng phải bao gồm hoặc loại trừ nó như là.

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


Hầu hết các lập trình viên có 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 để mảnh với nhau một chính xác thứ mười hoặc năm của pizza bằng cách sử dụng những lát đó, bất kể bạn cắt chúng ra sao. Bạn có thể thực hiện một phép tính xấp xỉ khá tốt và nếu bạn thêm xấp xỉ 0,1 với xấp xỉ 0,2, bạn nhận được xấp xỉ xấp xỉ 0,3, nhưng nó vẫn chỉ là một xấp xỉ.

Đối với các con số chính xác gấp đôi (độ chính xác cho phép bạn giảm một nửa số lượng pizza của bạn 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,1000000000000000055511151231257827021181583404541015625. Sau này là khá gần gũi hơn với 0,1 so với trước đây, do đó, một số phân tích cú pháp sẽ, đưa ra một đầu vào của 0,1, ủng hộ sau này.

(Sự khác biệt giữa hai con số này là "lát nhỏ nhất" mà chúng ta phải quyết định đưa vào, trong đó đưa ra một xu hướng đi lên hoặc loại trừ, điều này giới thiệu một xu hướng đi xuống. ulp.)

Trong trường hợp của 0,2, các con số đều giống nhau, chỉ cần nhân rộng bởi hệ số 2. Một lần nữa, chúng ta ưu tiên giá trị cao hơn một chút so với 0,2.

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

Cụ thể, 0,1 + 0,2 thực sự là 0,1000000000000000055511151231257827021181583404541015625 + 0,200000000000000011102230246251565404236316680908203125 = 0.3000000000000000444089209850062616169452667236328125, trong khi con số gần nhất với 0,3 thực sự là 0,299999999999999988897769753748434595763683319091796875.


P.S. Một số ngôn ngữ lập trình cũng cung cấp máy cắt bánh pizza có thể chia lát thành mười phần chính xác. Mặc dù máy cắt bánh pizza như vậy là không phổ biến, nếu bạn có quyền truy cập vào một, bạn nên sử dụng nó khi nó quan trọng để có thể nhận đượ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.)


226
2018-02-25 21:41



Lưu ý rằng có một số ngôn ngữ bao gồm toán học chính xác. Một ví dụ là Scheme, ví dụ thông qua GNU Guile. Xem draketo.de/english/exact-math-to-the-rescue - chúng giữ cho toán học dưới dạng phân số và chỉ cắt lên cuối. - Arne Babenhauserheide
@ FloatingRock Trên thực tế, rất ít ngôn ngữ lập trình chính thống có số tích hợp được tích hợp sẵn. Arne là một Schemer, như tôi, vì vậy đây là những điều chúng tôi nhận được hư hỏng trên. - Chris Jester-Young
@ArneBabenhauserheide Tôi nghĩ rằng nó có giá trị thêm rằng điều này sẽ chỉ làm việc với số lượng hợp lý. Vì vậy, nếu bạn đang làm một số toán học với các số vô tỉ như pi, bạn sẽ phải lưu trữ nó như là một bội số của pi. Tất nhiên, bất kỳ phép tính nào liên quan đến pi không thể được biểu diễn dưới dạng số thập phân chính xác. - Aidiakapi
@connexo Được rồi. Làm thế nào bạn sẽ lập trình xoay 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 một cách chính xác, bạn cũng có một máy cắt bánh pizza cắt chính xác.) Nói cách khác, bạn thực sự không thể có 1/360 (một mức độ) hoặc 1 / 10 (36 độ) chỉ với dấu chấm động nhị phân. - Chris Jester-Young
@connexo Ngoài ra, "mọi tên ngốc" không thể xoay bánh pizza chính xác 36 độ. Con người là quá dễ bị lỗi để làm bất cứ điều gì khá chính xác. - Chris Jester-Young


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


199
2018-04-09 12:25



máy tính không cần dung lượng bộ nhớ vô hạn để nhận được 0,1 + 0,2 = 0,3 - Pacerier
@Pacerier Chắc chắn, họ có thể sử dụng hai số nguyên chính xác không bị ràng buộc để đại diện cho một phần nhỏ, hoặc họ có thể sử dụng ký hiệu trích dẫn. Đó là khái niệm cụ thể của "nhị phân" hoặc "số thập phân" làm cho điều này là không thể - ý tưởng rằng bạn có một chuỗi các số nhị phân / thập phân và, ở đâu đó trong đó, một điểm radix. Để có đượ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
@Pacerier: Cả nhị phân và dấu phẩy động thập phân đều không 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 biểu mẫu M / 10 ^ E, nhưng ít 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 nói đến hầu hết các phân số khác. Trong nhiều ứng dụng, sẽ hữu ích hơn khi 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 tính năng "đặc biệt". - supercat
@Pacerier Họ làmnếu chúng đang lưu trữ các số dưới dạng nhị phân, đó là điểm của câu trả lời. - Mark Amery
@chux: Sự khác biệt về độ chính xác giữa các kiểu nhị phân và thập phân không lớn, nhưng sự khác biệt 10: 1 trong trường hợp tốt nhất so với trường hợp xấu nhất cho các loại thập phân lớn hơn nhiều so với sự khác biệt 2: 1 với các kiểu 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 viết để hoạt động hiệu quả trên một trong các loại thập phân hay không, vì dường như không có khả năng thực hiện hiệu quả trong phần cứng cũng như phần mềm. - supercat


Ngoài các câu trả lời đúng khác, bạn có thể cân nhắc việc mở 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

Cách diễn đạt 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ác lỗi biểu diễn thập phân có thể tránh được 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 khi độ chính xác là tối quan trọng, chúng tôi khuyên bạn nên1 để xử lý tiền như một số nguyên đại diện cho số xu: 2550 xu thay vì 25.50 USD.


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


99
2018-02-23 17:15



Vấn đề là bản thân sự biến đổi là không chính xác. 16.08 * 100 = 1607.9999999999998. Chúng ta có phải nghỉ mát để tách số và chuyển đổi riêng biệt (như trong 16 * 100 + 08 = 1608) không? - Jason
Giải pháp ở đây là làm tất cả các phép tính của bạn bằng số nguyên sau đó chia cho tỷ lệ của bạn (100 trong trường hợp này) và chỉ tròn khi trình bày dữ liệu. Điều đó sẽ đảm bảo rằng các phép tính của bạn sẽ luôn chính xác. - David Granado
Chỉ cần để nitpick một chút: số học số nguyên chỉ là chính xác trong điểm nổi lên đến một điểm (chơi chữ dự định). Nếu số 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à vì vậy 0x1p53 + 1 được làm tròn xuống 0x1p53 (và 0x1p53 + 3 được làm tròn thành 0x1p53 + 4, vì vòng-to-even). :-D Nhưng chắc chắn, nếu số của bạn nhỏ hơn 9 nghìn tỷ, bạn sẽ ổn thôi. :-P - Chris Jester-Young
Vậy làm thế nào để bạn có được .1 + .2 để hiển thị .3? - CodyBugstein
Jason, bạn nên làm tròn kết quả (int) (16.08 * 100 + 0.5) - Mikhail Semenov


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 chấm động, tôi đã đặt trọng tâm vào những gì máy thực sự làm. Tôi cũng đã làm cho nó cụ thể để tăng gấp đôi (64 bit) chính xác, nhưng đối số áp dụng như nhau cho bất kỳ số học dấu chấm động nào.

Lời nói đầu

An Định dạng dấu phẩy động nhị phân kép nhị phân IEEE 754 (nhị phân64) số đại diện cho một số biểu mẫu

giá trị = (-1) ^ s * (1.m51m50... m2m1m0)2 * 2e-1023

trong 64 bit:

  • Bit đầu tiên là dấu bit: 1 nếu con số là số âm, 0 nếu không thì1.
  • 11 bit tiếp theo là số mũ, đó là bù lại 1023. Nói cách khác, sau khi đọc các bit số mũ từ một số chính xác gấp đôi, 1023 phải được trừ để có được sức mạnh của hai.
  • 52 bit còn lại là significand (hoặc mantissa). Trong phần định nghĩa, một 'ngụ ý' 1. luôn luôn2 bỏ qua 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ề ký số không - - +0 và -0 được đối xử khác nhau: 1 / (+0) là vô cùng tích cực; 1 / (-0) là vô cực âm. Đối với các giá trị bằng không, các bit mantissa và số mũ là tất cả không. Lưu ý: giá trị bằng không (+0 và -0) rõ ràng không được phân loại là không bình thường2.

2 - Đây không phải là trường hợp số ngẫu nhiên, có số mũ bù bằng 0 (và ngụ ý 0.). Phạm vi của các số chính xác gấp đôi không chính xác là dphút ≤ | x | ≤ dtối đa, nơi dphút (số nonzero đại diện nhỏ nhất) là 2-1023 - 51 (≈ 4,94 * 10-324) và dtối đa (số lượng bất thường lớn nhất, trong đó mantissa bao gồm hoàn toàn 1s) là 2-1023 + 1 - 2-1023 - 51 (≈ 2.225 * 10-308).


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

Nhiều trình chuyển đổi trực tuyến tồn tại để chuyển đổi số điểm nổi chính xác gấp đôi thành nhị phân (ví dụ: binaryconvert.com), nhưng đây là một số mẫu C # mã để có được đại diện IEEE 754 cho một số chính xác gấp đôi (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

(Chuyển xuống dưới cùng cho phiên bản TL; DR)

Cato Johnston (người hỏi câu hỏi) hỏi tại sao 0.1 + 0.2! = 0.3.

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

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

Lưu ý rằng mantissa bao gồm các chữ số định kỳ của 0011. Đây là Chìa khóa 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 trong nhị phân đúng trong một có hạn số bit nhị phân bất kỳ hơn 1/9, 1/3 hoặc 1/7 có thể được biểu diễn chính xác trong chữ số thập phân.

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

0.1 = 2^-4 * [1].1001100110011001100110011001100110011001100110011010
0.2 = 2^-3 * [1].1001100110011001100110011001100110011001100110011010

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

0.1 = 2^-3 *  0.1100110011001100110011001100110011001100110011001101(0)
0.2 = 2^-3 *  1.1001100110011001100110011001100110011001100110011010
sum = 2^-3 * 10.0110011001100110011001100110011001100110011001100111

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

sum = 2^-2 * 1.0011001100110011001100110011001100110011001100110011(1)

Hiện tại có 53 bit trong phần định trị (thứ 53 nằm trong dấu ngoặc vuông trong dòng trên). Mặc định chế độ làm tròn cho IEEE 754 là 'Vòng tới gần nhất'- tức là nếu một số x nằm giữa hai giá trị một và b, giá trị mà bit ít quan trọng nhất là số không được chọn.

a = 2^-2 * 1.0011001100110011001100110011001100110011001100110011
x = 2^-2 * 1.0011001100110011001100110011001100110011001100110011(1)
b = 2^-2 * 1.0011001100110011001100110011001100110011001100110100

Lưu ý rằng một 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 ít quan trọng nhất là b, vì vậy tổng là:

sum = 2^-2 * 1.0011001100110011001100110011001100110011001100110100

TL; DR

Viết 0.1 + 0.2 trong một đại diện nhị phân IEEE 754 (với 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]

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

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

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

So sánh vài bit cuối cùng của một số dấu phẩy động vốn đã nguy hiểm, như bất cứ ai đọc nổi tiếng "Mỗi nhà khoa học máy tính nên biết gì về số học dấu chấm độ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 thêm số bảo vệ để giải quyết vấn đề này, đó là cách 0.1 + 0.2 sẽ cho 0.3: các bit cuối cùng được làm tròn.


81
2018-03-16 05:27



Câu trả lời của tôi đã được bình chọn ngay sau khi đăng nó. Tôi đã thực hiện nhiều thay đổi (bao gồm một cách rõ ràng lưu ý các bit định kỳ khi viết 0,1 và 0,2 trong nhị phân, mà tôi muốn bỏ qua trong bản gốc). Trên cơ hội ra rằng cử tri xuống 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 tôi? Tôi cảm thấy rằng câu trả lời của tôi thêm một cái gì đó mới kể từ khi điều trị tổng trong IEEE 754 không được bảo hiểm trong cùng một cách trong câu trả lời khác. Trong khi "Mọi nhà khoa học máy tính nên biết ..." bao gồm một số tài liệu giống nhau, các giao dịch trả lời của tôi đặc biệt với trường hợp 0,1 + 0,2. - Wai Ha Lee