Tại sao số dấu phẩy động không chính xác?


198

Tại sao một số số mất độ chính xác khi được lưu trữ dưới dạng số dấu phẩy động?

Ví dụ, số thập phân 9.2có thể được biểu thị chính xác theo tỷ lệ của hai số nguyên thập phân ( 92/10), cả hai số này có thể được biểu thị chính xác dưới dạng nhị phân ( 0b1011100/0b1010). Tuy nhiên, tỷ lệ tương tự được lưu trữ dưới dạng số dấu phẩy động không bao giờ chính xác bằng 9.2:

32-bit "single precision" float: 9.19999980926513671875
64-bit "double precision" float: 9.199999999999999289457264239899814128875732421875

Làm thế nào một số rõ ràng đơn giản như vậy có thể "quá lớn" để thể hiện trong 64 bit bộ nhớ?




Câu trả lời:


241

Trong hầu hết các ngôn ngữ lập trình, các số dấu phẩy động được thể hiện rất giống ký hiệu khoa học : với số mũ và mantissa (còn được gọi là ý nghĩa). Một con số rất đơn giản 9.2, thực sự là phần này:

5179139571476070 * 2 -49

Trường hợp số mũ là -49và mantissa là 5179139571476070. Lý do không thể biểu diễn một số số thập phân theo cách này là vì cả số mũ và số mũ phải là số nguyên. Nói cách khác, tất cả các số float phải là một số nguyên nhân với công suất nguyên là 2 .

9.2có thể đơn giản 92/10, nhưng 10 không thể được biểu thị bằng 2 n nếu n bị giới hạn ở các giá trị nguyên.


Xem dữ liệu

Đầu tiên, một vài chức năng để xem các thành phần tạo ra 32 và 64 bit float. Đưa ra những điều này nếu bạn chỉ quan tâm đến đầu ra (ví dụ trong Python):

def float_to_bin_parts(number, bits=64):
    if bits == 32:          # single precision
        int_pack      = 'I'
        float_pack    = 'f'
        exponent_bits = 8
        mantissa_bits = 23
        exponent_bias = 127
    elif bits == 64:        # double precision. all python floats are this
        int_pack      = 'Q'
        float_pack    = 'd'
        exponent_bits = 11
        mantissa_bits = 52
        exponent_bias = 1023
    else:
        raise ValueError, 'bits argument must be 32 or 64'
    bin_iter = iter(bin(struct.unpack(int_pack, struct.pack(float_pack, number))[0])[2:].rjust(bits, '0'))
    return [''.join(islice(bin_iter, x)) for x in (1, exponent_bits, mantissa_bits)]

Có rất nhiều điều phức tạp đằng sau chức năng đó và nó sẽ khá là khó để giải thích, nhưng nếu bạn quan tâm, tài nguyên quan trọng cho các mục đích của chúng tôi là mô-đun struct .

Python floatlà một số chính xác 64 bit. Trong các ngôn ngữ khác như C, C ++, Java và C #, độ chính xác kép có một loại riêng double, thường được triển khai là 64 bit.

Khi chúng ta gọi hàm đó bằng ví dụ của mình 9.2, đây là những gì chúng ta nhận được:

>>> float_to_bin_parts(9.2)
['0', '10000000010', '0010011001100110011001100110011001100110011001100110']

Giải thích dữ liệu

Bạn sẽ thấy tôi chia giá trị trả về thành ba thành phần. Các thành phần này là:

  • Ký tên
  • Số mũ
  • Mantissa (còn được gọi là Ý nghĩa hoặc Phân số)

Ký tên

Dấu hiệu được lưu trữ trong thành phần đầu tiên dưới dạng một bit. Thật dễ để giải thích: 0có nghĩa là số float là một số dương; 1có nghĩa là nó tiêu cực. Bởi vì 9.2là tích cực, giá trị dấu hiệu của chúng tôi là 0.

Số mũ

Số mũ được lưu trữ trong thành phần giữa là 11 bit. Trong trường hợp của chúng tôi , 0b10000000010. Trong thập phân, điều đó đại diện cho giá trị 1026. Một điều khó hiểu của thành phần này là bạn phải trừ đi một số bằng 2 (# bit) - 1 - 1 để có được số mũ thực sự; trong trường hợp của chúng tôi, điều đó có nghĩa là trừ 0b1111111111(số thập phân 1023) để có được số mũ thực sự, 0b00000000011(số thập phân 3).

Thần chú

Lớp phủ được lưu trữ trong thành phần thứ ba là 52 bit. Tuy nhiên, cũng có một sự giải thích cho thành phần này. Để hiểu vấn đề này, hãy xem xét một số trong ký hiệu khoa học, như thế này:

6.0221413x10 23

Bọ ngựa sẽ là 6.0221413. Hãy nhớ lại rằng mantissa trong ký hiệu khoa học luôn bắt đầu bằng một chữ số khác không. Điều tương tự cũng đúng với nhị phân, ngoại trừ nhị phân chỉ có hai chữ số: 01. Vì vậy, mantissa nhị phân luôn luôn bắt đầu với 1! Khi một float được lưu trữ, 1ở phía trước của mantissa nhị phân được bỏ qua để tiết kiệm không gian; chúng ta phải đặt nó trở lại ở phía trước của yếu tố thứ ba của chúng ta để có được câu thần chú thực sự :

1,0010011001100110011001100110011001100110011001100110

Điều này liên quan đến nhiều hơn chỉ là một bổ sung đơn giản, bởi vì các bit được lưu trữ trong thành phần thứ ba của chúng ta thực sự đại diện cho phần phân đoạn của lớp phủ, ở bên phải của điểm cơ số .

Khi xử lý các số thập phân, chúng ta "di chuyển dấu thập phân" bằng cách nhân hoặc chia cho lũy thừa 10. Trong nhị phân, chúng ta có thể làm điều tương tự bằng cách nhân hoặc chia cho lũy thừa của 2. Vì phần tử thứ ba của chúng ta có 52 bit, chúng ta chia nó bằng 2 52 để di chuyển 52 vị trí sang phải:

0,0010011001100110011001100110011001100110011001100110

Trong ký hiệu thập phân, đó là giống như chia 675539944105574bởi 4503599627370496để có được 0.1499999999999999. (Đây là một ví dụ về tỷ lệ có thể được biểu thị chính xác dưới dạng nhị phân, nhưng chỉ xấp xỉ bằng số thập phân; để biết thêm chi tiết, xem: 675539944105574/4503599627370496 .)

Bây giờ chúng tôi đã chuyển đổi thành phần thứ ba thành một số phân số, việc thêm vào 1sẽ tạo ra lớp phủ thực sự.

Tóm tắt lại các thành phần

  • Dấu hiệu (thành phần đầu tiên): 0cho tích cực, 1cho tiêu cực
  • Số mũ (thành phần giữa): Trừ 2 (# bit) - 1 - 1 để lấy số mũ thực
  • Mantissa (thành phần cuối cùng): Chia cho 2 (# bit) và thêm 1vào để có được mantissa thực sự

Tính số

Đặt cả ba phần lại với nhau, chúng tôi đã cho số nhị phân này:

1,0010011001100110011001100110011001100110011001100110 x 10 11

Mà sau đó chúng ta có thể chuyển đổi từ nhị phân sang thập phân:

1.1499999999999999 x 2 3 (không chính xác!)

Và nhân lên để hiển thị đại diện cuối cùng của số chúng tôi bắt đầu bằng ( 9.2) sau khi được lưu trữ dưới dạng giá trị dấu phẩy động:

9.1999999999999993


Đại diện như một phân số

9,2

Bây giờ chúng tôi đã tạo số, có thể xây dựng lại thành một phần đơn giản:

1,0010011001100110011001100110011001100110011001100110 x 10 11

Chuyển mantissa sang một số nguyên:

10010011001100110011001100110011001100110011001100110 x 10 11-110100

Chuyển đổi sang số thập phân:

5179139571476070 x 2 3-52

Trừ đi số mũ:

5179139571476070 x 2 -49

Biến số mũ âm thành chia:

5179139571476070/2 49

Nhân số mũ:

5179139571476070/56949953421312

Mà bằng:

9.1999999999999993

9,5

>>> float_to_bin_parts(9.5)
['0', '10000000010', '0011000000000000000000000000000000000000000000000000']

Bạn đã có thể thấy mantissa chỉ có 4 chữ số theo sau là rất nhiều số không. Nhưng chúng ta hãy đi qua các bước.

Lắp ráp ký hiệu khoa học nhị phân:

1,0011 x 10 11

Thay đổi dấu thập phân:

10011 x 10 11-100

Trừ đi số mũ:

10011 x 10 -1

Nhị phân đến thập phân:

19 x 2 -1

Số mũ âm để chia:

19/2 1

Nhân số mũ:

19/2

Bằng:

9,5



đọc thêm


1
Ngoài ra còn có một hướng dẫn tốt đẹp mà chương trình làm thế nào để đi theo con đường khác - được đưa ra một đại diện thập phân của một số, làm thế nào để bạn xây dựng các điểm tương đương nổi. Cách tiếp cận "phân chia dài" cho thấy rất rõ cách bạn kết thúc với "phần còn lại" sau khi cố gắng đại diện cho số. Nên được thêm vào nếu bạn muốn thực sự "kinh điển" với câu trả lời của bạn.
Floris

1
Nếu bạn đang nói về Python và dấu phẩy động, tôi sẽ đề xuất ít nhất là bao gồm hướng dẫn Python trong các liên kết của bạn: docs.python.org/3.4/tutorial/floatingpoint.html Điều đó được coi là một điểm dừng tài nguyên cho các vấn đề dấu phẩy động cho các lập trình viên Python. Nếu nó thiếu một cách nào đó (và gần như chắc chắn là vậy), vui lòng mở một sự cố trên trình theo dõi lỗi Python để cập nhật hoặc thay đổi.
Mark Dickinson

@mhlester Nếu điều này được chuyển thành wiki cộng đồng, vui lòng kết hợp câu trả lời của tôi vào của bạn.
Nicu Stiurca

5
Câu trả lời này chắc chắn cũng nên liên kết đến float-point-gui.de , vì đây có lẽ là phần giới thiệu tốt nhất cho người mới bắt đầu. IMO, nó thậm chí còn vượt lên trên "Điều mà mọi nhà khoa học máy tính nên biết ..." - ngày nay, những người có thể hiểu một cách hợp lý bài báo của Goldberg thường đã nhận thức rõ về nó.
Daniel Pryden

1
"Đây là một ví dụ về tỷ lệ có thể được biểu thị chính xác dưới dạng nhị phân, nhưng chỉ xấp xỉ bằng số thập phân". Đây không phải là sự thật. Tất cả các số này trên một tỷ số lũy thừa đều chính xác theo số thập phân. Mọi phép tính gần đúng chỉ để rút ngắn số thập phân - để thuận tiện.
Rick Regan

29

Đây không phải là một câu trả lời đầy đủ ( mhlester đã bao gồm rất nhiều nền tảng tốt mà tôi sẽ không trùng lặp), nhưng tôi muốn nhấn mạnh mức độ đại diện của một số phụ thuộc vào cơ sở bạn đang làm việc.

Xét phân số 2/3

Trong cơ sở 10 tốt, chúng tôi thường viết nó ra như một cái gì đó như

  • 0,666 ...
  • 0,666
  • 0,667

Khi chúng ta nhìn vào các biểu diễn đó, chúng ta có xu hướng liên kết mỗi phần tử đó với phân số 2/3, mặc dù chỉ có biểu diễn đầu tiên về mặt toán học bằng với phân số. Các đại diện / xấp xỉ thứ hai và thứ ba có lỗi theo thứ tự 0,001, thực sự tồi tệ hơn nhiều so với lỗi giữa 9.2 và 9.1999999999999993. Trong thực tế, đại diện thứ hai thậm chí không được làm tròn chính xác! Tuy nhiên, chúng tôi không có vấn đề gì với 0,666 là xấp xỉ số 2/3, vì vậy chúng tôi thực sự không có vấn đề gì với cách 9.2 gần đúng trong hầu hết các chương trình . (Vâng, trong một số chương trình, nó quan trọng.)

Số căn cứ

Vì vậy, đây là nơi căn cứ số là rất quan trọng. Nếu chúng ta cố gắng đại diện cho 2/3 trong cơ sở 3, thì

(2/3) 10 = 0,2 3

Nói cách khác, chúng ta có một đại diện chính xác, hữu hạn cho cùng một số bằng cách chuyển đổi cơ sở! Điều đáng nói là mặc dù bạn có thể chuyển đổi bất kỳ số nào sang bất kỳ cơ sở nào, tất cả các số hữu tỷ đều có các biểu diễn hữu hạn chính xác trong một số cơ sở nhưng không phải ở các số khác .

Để lái xe về điểm này, hãy nhìn vào 1/2. Nó có thể làm bạn ngạc nhiên rằng mặc dù con số hoàn toàn đơn giản này có một đại diện chính xác trong cơ sở 10 và 2, nó yêu cầu một đại diện lặp lại trong cơ sở 3.

(1/2) 10 = 0,5 10 = 0,1 2 = 0.111 ... 3

Tại sao số dấu phẩy động không chính xác?

Bởi vì thông thường, chúng là các số hữu tỷ gần đúng không thể được biểu diễn chính xác trong cơ sở 2 (lặp lại các chữ số) và nói chung chúng xấp xỉ các số thực (có thể không hợp lý) có thể không thể biểu thị bằng nhiều chữ số trong bất kỳ cơ sở nào .


3
Vì vậy, nói cách khác, cơ sở 3 sẽ hoàn hảo 1/3cơ sở 10 là hoàn hảo 1/10. Không phân số nào hoạt động trong cơ sở 2
mhlester

2
@mhlester Vâng. Và nói chung, cơ sở N là hoàn hảo cho bất kỳ phân số nào có mẫu số là Nhoặc bội số của chúng.
Nicu Stiurca

2
Và đây là một lý do tại sao một số hộp công cụ số theo dõi "phần được chia cho cái gì" và trong quá trình này có thể giữ "độ chính xác vô hạn" cho tất cả các số hữu tỷ. Giống như các nhà vật lý muốn giữ các phương trình của họ là biểu tượng cho đến thời điểm cuối cùng có thể, trong trường hợp các yếu tố của πvv hủy bỏ.
Floris

3
@Floris Tôi cũng đã thấy các trường hợp thuật toán chỉ thực hiện số học cơ bản (nghĩa là bảo toàn tính hợp lý của đầu vào), xác định xem đầu vào có hợp lý hay không, thực hiện phép toán bằng số học dấu phẩy động thông thường, sau đó ước tính lại một tỷ lệ hợp lý xấp xỉ ở cuối để sửa bất kỳ lỗi làm tròn. Cụ thể, thuật toán hình thức hồi âm hàng giảm của Matlab thực hiện điều này và nó giúp ổn định số lượng rất nhiều.
Nicu Stiurca

@SchighSchagh - thật thú vị, tôi không biết điều đó. Tôi biết rằng sự ổn định về số là thứ không được dạy đầy đủ trong những ngày có độ chính xác gấp đôi. Điều đó có nghĩa là nhiều người bỏ lỡ việc học về sự thanh lịch của nhiều thuật toán đẹp. Tôi thực sự thích các thuật toán tính toán và sửa lỗi của chính họ.
Floris

13

Trong khi tất cả các câu trả lời khác đều tốt, vẫn còn một điều còn thiếu:

Nó là không thể để đại diện cho số vô tỉ (ví dụ π, sqrt(2), log(3), vv) một cách chính xác!

Và đó thực sự là lý do tại sao chúng được gọi là phi lý. Không có dung lượng lưu trữ bit nào trên thế giới sẽ đủ để chứa một trong số chúng. Chỉ có số học tượng trưng là có thể bảo tồn độ chính xác của chúng.

Mặc dù nếu bạn sẽ giới hạn nhu cầu toán học của mình ở những con số hợp lý thì chỉ có vấn đề về độ chính xác mới có thể kiểm soát được. Bạn sẽ cần lưu trữ một cặp số nguyên (có thể rất lớn) abđể giữ số được biểu thị bằng phân số a/b. Tất cả số học của bạn sẽ phải được thực hiện trên các phân số giống như trong toán học trung học (ví dụ a/b * c/d = ac/bd).

Nhưng tất nhiên bạn vẫn sẽ chạy vào cùng một loại rắc rối khi pi, sqrt, log, sinvv có liên quan.

TL; DR

Đối với số học tăng tốc phần cứng, chỉ có thể biểu diễn một số lượng hợp lý giới hạn. Mỗi số không đại diện được xấp xỉ. Một số số (tức là không hợp lý) không bao giờ có thể được biểu diễn cho dù hệ thống.


4
Thật thú vị, các cơ sở phi lý tồn tại. Giả mạo , ví dụ.
Veedrac

5
số vô tỷ có thể (chỉ) được biểu diễn trong cơ sở của chúng. Ví dụ pi là 10 trong cơ sở pi
phuclv

4
Điểm vẫn còn hiệu lực: Một số số không bao giờ có thể được đại diện cho dù hệ thống. Bạn không đạt được bất cứ điều gì bằng cách thay đổi cơ sở của mình vì sau đó một số số khác không thể được biểu diễn nữa.
LumpN

4

Có vô số số thực (rất nhiều số mà bạn không thể liệt kê chúng) và có vô số số hữu tỷ (có thể liệt kê chúng).

Biểu diễn dấu phẩy động là một biểu thức hữu hạn (giống như bất cứ thứ gì trong máy tính), vì vậy không thể tránh khỏi nhiều đại lượng nhiều số không thể biểu diễn. Cụ thể, 64 bit chỉ cho phép bạn phân biệt giữa 18,446,744,073,709,551,616 giá trị khác nhau (không là gì so với vô cùng). Với quy ước tiêu chuẩn, 9.2 không phải là một trong số đó. Những số có thể có dạng m.2 ^ e cho một số số nguyên m và e.


Bạn có thể đưa ra một hệ thống số khác nhau, ví dụ 10 dựa trên, trong đó 9.2 sẽ có một đại diện chính xác. Nhưng những con số khác, nói 1/3, vẫn sẽ không thể đại diện.


Cũng lưu ý rằng các số dấu phẩy động chính xác kép là cực kỳ chính xác. Chúng có thể đại diện cho bất kỳ số nào trong một phạm vi rất rộng với tối đa 15 chữ số chính xác. Đối với các tính toán cuộc sống hàng ngày, 4 hoặc 5 chữ số là quá đủ. Bạn sẽ không bao giờ thực sự cần 15 cái đó, trừ khi bạn muốn đếm từng mili giây trong đời.


1

Tại sao chúng ta không thể biểu diễn 9,2 trong dấu phẩy động nhị phân?

Các số dấu phẩy động là (đơn giản hóa một chút) một hệ thống đánh số vị trí với số lượng chữ số bị hạn chế và một điểm cơ số có thể di chuyển.

Một phân số chỉ có thể được biểu thị chính xác bằng cách sử dụng số chữ số hữu hạn trong hệ thống đánh số vị trí nếu các thừa số nguyên tố của mẫu số (khi phân số được biểu thị theo thuật ngữ thấp nhất) là các yếu tố của cơ sở.

Các thừa số nguyên tố của 10 là 5 và 2, vì vậy trong cơ sở 10 chúng ta có thể biểu diễn bất kỳ phần nào có dạng a / (2 b 5 c ).

Mặt khác, thừa số nguyên tố duy nhất của 2 là 2, vì vậy trong cơ sở 2 chúng ta chỉ có thể biểu diễn các phân số có dạng a / (2 b )

Tại sao máy tính sử dụng đại diện này?

Bởi vì đó là một định dạng đơn giản để làm việc và nó đủ chính xác cho hầu hết các mục đích. Về cơ bản cùng một lý do các nhà khoa học sử dụng "ký hiệu khoa học" và làm tròn kết quả của họ thành một số chữ số hợp lý ở mỗi bước.

Chắc chắn có thể định nghĩa một định dạng phân số, với (ví dụ) tử số 32 bit và mẫu số 32 bit. Nó có thể biểu diễn các số mà điểm nổi chính xác kép của IEEE không thể, nhưng cũng có thể có nhiều số có thể được biểu thị theo dấu phẩy động chính xác kép không thể được biểu thị theo định dạng phân số có kích thước cố định như vậy.

Tuy nhiên, vấn đề lớn là định dạng như vậy là một nỗi đau để tính toán. Vì hai lý do.

  1. Nếu bạn muốn có chính xác một đại diện cho mỗi số thì sau mỗi phép tính, bạn cần giảm phân số về các số hạng thấp nhất. Điều đó có nghĩa là đối với mọi hoạt động về cơ bản bạn cần thực hiện một phép tính ước số chung lớn nhất.
  2. Nếu sau khi tính toán, bạn kết thúc với một kết quả không thể biểu thị được vì tử số hoặc mẫu số bạn cần tìm kết quả đại diện gần nhất. Điều này là không tầm thường.

Một số Ngôn ngữ cung cấp các loại phân số, nhưng thông thường chúng thực hiện kết hợp với độ chính xác tùy ý, điều này tránh phải lo lắng về xấp xỉ các phân số nhưng nó tạo ra vấn đề của riêng nó, khi một số vượt qua một số lượng lớn các bước tính toán của mẫu số và do đó lưu trữ cần thiết cho phân số có thể phát nổ.

Một số ngôn ngữ cũng cung cấp các loại dấu phẩy động thập phân, chúng chủ yếu được sử dụng trong các tình huống quan trọng là kết quả mà máy tính có được phù hợp với các quy tắc làm tròn có sẵn được viết với con người (chủ yếu là tính toán tài chính). Chúng hơi khó làm việc hơn so với dấu phẩy động nhị phân, nhưng vấn đề lớn nhất là hầu hết các máy tính không cung cấp hỗ trợ phần cứng cho chúng.


-4

Thử cái này

DecimalFormat decimalFormat = new DecimalFormat("#.##");
String.valueOf(decimalFormat.format(decimalValue))));

' decimalValue' là giá trị của bạn để chuyển đổi.

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.