Tại sao cùng 1 biểu thức 0.1+0.2!=0.3 trong khi gán cho biến float thì kết quả so sánh là khác nhau?

hi các bác, tình hình là em có 4 thắc mắc, mong các bác có thể giải đáp giúp em, em cảm ơn.
Đầu tiên là:
em chưa rõ vì sao 0.1+0.2!=0.3 trong khi x+x2=y2 khi gán x=0.1, 0.2, y2=0.3?
cái 0.1+0.2!=0.3 thì em có google đoán được là do số 0.2 ở dạng nhị phân là số nhị phân vô hạn, cho nên khi chuyển về decimal thì nó bị mất bit cho nên không biểu diễn chính xác được.

Thắc mắc 2 là:
theo suy nghĩ tương tự 0.2 bị mất bit khi về decimal vậy tại sao 0.2+0.2 lại bằng 0.4?

Thắc mắc 3 là:
khi viết cout<<(0.2+0.1), thì ở ngôn ngữ c++ nó coi 0.1, 0.2 là kiểu float hay double hay kiểu gì ạ?

Thắc mắc 4 là: cho 2 số float point ( số thập phân) bất kỳ, liệu có đoán trước được phép cộng của chúng trong máy tính có bằng với giá trị sum trong toán học hay không?
ví dụ liệu có đoán trước được 0.1+0.5 có bằng 0.6 không? hay 0.1+0.3 có bằng 0.4 hay không?

cái IEEE754 em đọc wiki cũng ko hiểu lắm, cái sau nó viết nhiều về toán, nên em chỉ hiểu sơ sở là:
nó sẽ tìm fraction và exponent cộng thêm bit dấu là đủ số bit được cấp phát.
tuy nhiên IEEE754 nó không nói cách 2 số float point được với nhau diễn ra như thế nào?, nó cũng không nói vì sao ở dạng x=0.1, x2=0.2, y=0.3 thì x+x2=y, nó chỉ nói cách 0.1 được lưu ở binary dưới form như nào thôi.

em xin cảm ơn các bác nhé


nếu bạn khai báo biến kiểu double thì sẽ ra lỗi. float thì không.
Bạn tìm hiểu thêm cách biểu diễn kiểu float và double

1 Like


bạn đang trả lời thắc mắc 1 đúng ko ạ, theo ảnh này, em có sửa dòng 9,10,11 thành double thì được kết quả so sánh là 0.
còn thắc mắc 2, bác có thể giải đáp cho em được ko ạ,

số thực thì máy tính tính cũng chỉ chứa được tới x chữ số thôi :V float thì chứa ít chữ số hơn, 24 chữ số nhị phân ~7 chữ số thập phân. double chứa nhiều chữ số hơn, 53 chữ số nhị phân ~16 chữ số thập phân.

1/3 ở hệ thập phân ko biểu diễn được vì 10 ko chia hết cho 3, nên 1/3 = 0.3333… Tương tự 1/7 1/6 1/9 1/13 v.v… cũng ko biểu diễn được ở hệ thập phân. Hệ nhị phân tương tự :V còn khó biểu diễn tròn số như hệ thập phân :V 0.1, 0.2, 0.3 đều ko biểu diễn tròn số được ở hệ nhị phân.

để biểu diễn số vô hạn tuần hoàn này thì ta có thể viết () cho dãy tuần hoàn :V
ví dụ

  • 1/3 = 0.(3) = 0.0 + 3/9
  • 1/6 = 0.1(6) = 0.1 + 6/90
  • 1/33 = 0.(03) = 3/99

để ý thấy là có n số mở ngoặc ở trong thì lấy số đó chia cho n số 9 hay (10^n - 1) là ra :V ngoài ra còn phải đếm nó cách dấu thập phân m số thì phải chia cho 10^m nữa.

vậy ở hệ nhị phân thì

  • 0.1 = 0.0(0011): trong () có 4 chữ số, giá trị là 0011 = 3, vậy lấy 3 / (2^4-1) = 3/15 = 0.2, mà () cách dấu . thập phân 1 số, vậy chia 2^1 là chia 2 nữa là = 0.1
  • 0.2 = 0.(0011): trong () có 4 chữ số, giá trị là 0011 = 3, vậy lấy 3 / (2^4-1) = 3/15 = 0.2, () cách dấu . thập phân 0 số khỏi cần chia, giá trị là 0.2
  • 0.3 = 0.0(1001): trong () có 4 chữ số, giá trị là 1001 = 9, vậy lấy 9 / (2^4-1) = 9/15 = 0.6, mà () cách dấu . thập phân 1 số, vậy chia 2^1 là chia 2 nữa là = 0.3

viết ra dưới dạng vô hạn

0.1 = 0.0(0011) = 0.000110011001100110011001100110011...
0.2 = 0.(0011)  = 0.001100110011001100110011001100110...
0.3 = 0.0(1001) = 0.010011001100110011001100110011001...

máy tính ko lấy dấu . thập phân xa thế kia, ví dụ ở hệ thập phân ta viết 0.01234 thì có thể viết lại ở dạng 1.234 x 10^-2, làm sao cho số trước thập phân < 10. Tương tự ở hệ nhị phân, số trước dấu . phải là số < 2, ở đây chỉ có thể là số 1. Vậy viết lại

0.1 = 0.0(0011) = 1. 1001 1001 1001 1001 1001 1001 1001 1... x 2^-4
0.2 = 0.(0011)  = 1. 1001 1001 1001 1001 1001 1001 1001 1... x 2^-3
0.3 = 0.0(1001) = 1. 0011 0011 0011 0011 0011 0011 0011 0... x 2^-2

do số trước dấu thập phân luôn là số 1 nên có thể bỏ qua, với float chỉ lưu được 23 chữ số sau dấu ., là

1001 1001 1001 1001 1001 101 với 0.1
1001 1001 1001 1001 1001 101 với 0.2
0011 0011 0011 0011 0011 010 với 0.3

lưu ý ở đây vì phần bit bỏ đi là 1001 thì bit đầu tiên bị bỏ đi là bit 1 nên nó sẽ làm tròn 100 thành 101. Tương tự 0.3 có 0011 cũng bị làm tròn thành 010.

với double thì nó lưu được tới 52 bit sau dấu .

1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1010 với 0.1
1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1010 với 0.2
0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 với 0.3

lưu ý 0.1 và 0.2 sẽ được làm tròn lên: 10011 thành 1010, còn 0.3 bị làm tròn xuống: 00110 thành 0011

đây là lý do dẫn đến so sánh == bị sai ở double vì 0.1 0.2 làm tròn lên thành số > 0.1 và 0.2, còn 0.3 bị làm tròn xuống thành số < 0.3, nên 0.1 + 0.2 > 0.3 ở double. Còn float thì có thể đúng vì cả 3 số đều được làm tròn lên (thật ra còn phải làm phép tính 0.1 + 0.2 nữa mà viết dài lười quá :V)

edit: ko chắc cái quy luật làm tròn của floatdouble lắm, tại có nhiều quy luật :V nhưng ví dụ quy luật gặp 0 thì bỏ gặp 1 thì +1 vào này là đủ để thấy là do làm tròn nên có sai số rồi nha :V

4 Likes

hay quá, em vẫn đang đọc, em sẽ có câu hỏi sau khi đọc hiểu xong

về phần + thì em google “floating point addition” là ra nhiều lắm :V chọn mấy trang có đuôi “.edu” mà đọc rồi lấy giấy ra cộng 0.1 với 0.2 float xem nó có == thật ko :V double thì chắc chắn là khác rồi khỏi tính :V

1 Like

vậy là với 0.1 và 0.2 thì phần fraction thì đều giống nhau, vậy cái làm cho giá trị nó khác nhau là ở exponent phải ko ạ?
khi mình cần so sánh liệu tổng 2 số float point bất kỳ có bằng tổng với số đó khi ở toán học không , thì mình phải chuyển về dạng binary, tiếp đó làm tròn, sau đó cộng chúng lại, rồi mới có thể biết được ạ?


e cần convert 1 số float point sang số decimal, nhưng e không rõ ở kiểu long double có 16 byte, thì có công thức nào hoặc đoạn code nào in ra được phần exponent ở kiểu long double có bao nhiêu bit không ạ?
với double thì e biết là có 11 bit cho exponent

Chuyển kiểu lớn sang kiểu nhỏ mới sợ bị mất bit, còn kiểu nhỏ sang kiểu lớn thì vẫn không có vấn đề gì chứ.

1 Like

đúng á bạn, bạn đang hiểu nhầm mình hỏi.
mình hỏi với kiểu long double có 16 byte thì nó có bao nhiêu bit cho phần exponent ý bạn,
ý mình người ta có công thức hoặc quy luật xác định số bit cho phần exponent không á?
nếu ko có công thức tức là , ban đầu người ta đã set thế rồi vậy thì có đoạn code c++ nào in ra được phần exponent của long double có bao nhiêu bit cho ko ạ?
mục đích mình tìm exponet để mình có thể tính bằng tay xem giá trị của float point từ binary sang dạng decimal sẽ là bao nhiêu.
e search chưa ra.

Về long double:

As with C’s other floating-point types, it may not necessarily map to an IEEE format.

Vấn đề là chưa chắc long double đã biểu diễn theo IEEE format nên bạn có tính tay thì cũng chưa chắc giống với cách biểu diễn của máy.

long double thường có phần precision là 80 bit, cộng thêm 1 bit dấu thì bạn có thể trừ đi còn 47 bit exponent.

Nếu bạn chỉ muốn kiểm chứng bạn đổi tay 1 số thập phân sang IEEE format có đúng hay không thì bạn lên mạng kiểm tra còn hơn.

https://www.binaryconvert.com/convert_float.html

https://www.binaryconvert.com/convert_double.html


Ngó sang AIX operating system cũng thấy số 128-bit floating point cũng khác:

https://www.ibm.com/docs/en/aix/7.1?topic=sepl-128-bit-long-double-floating-point-data-type

hình như bạn nói hơi nhầm hoặc khó hiểu 1 xíu, vì precision , mình đọc wiki thì nó là tổng các bit của: sign + exponent+ fraction.



còn

hi bác
em không hiểu bác nói gì lắm, cent ở đây ý bạn là 1% +2%= 3%?

currency ý bác đang nói gì ạ?

hay decimal tùy ngôn ngữ)

cũng ko hiểu Bác đang muốn nói điều gì

Đoạn này em hỏi là em muốn hỏi cái tương tự như 0.1+0.2=0.3, là thắc mắc số 4 ạ
lệu em cout<< , 1 biểu thức bất kỳ như: cout<<((0.4+0.3)=0.7), liệu ta có thể biết trước được kết quả mà không phải tính bằng tay theo step như em nói hay không?
(em ko hỏi so sánh float trên máy tính nên làm ntn để tránh lỗi như 0.1+0.2!=0.3)

Hi @anonymous253 aka @iloveprogramming

Mình tìm hiểu thì

(src)

còn precision là 1 câu chuyện khác.

0.4 = 0.2 * 2, exponent của 0.4 = exponent của 0.2 - 1 nên không nảy sinh thêm sai sót.

Mặc định không có prefix thì đó là double literal.

Vậy bạn phải đi tìm hiểu các tài liệu khoa học khác.

https://www.sciencedirect.com/topics/engineering/floating-point-number#:~:text=The%20exponents%20of%20floating%20point,point%20numbers%20is%20as%20follows%3A&text=Shift%20the%20smaller%20number%20to,smaller%20number%20after%20each%20shift

Không biết bạn đã đọc mục 5. Operations trên IEEE 754-2008 chưa nhỉ?

Không biết có phải không, nhưng mình nghĩ phép cộng trừ nhân chia không đơn giản chỉ là tính; cũng liên quan đến CPU nữa.

1 Like

em chưa có thời gian hay điều kiện đọc Bác, em mới chỉ tìm hiểu nhanh trên IEEE754 trên wiki, như bác tntxtnt nói cái em hiểu ra phần nào luôn

dòng này em thấy chưa clear bác đọc cái này ở đâu hay suy ra như thế nào ạ?

đoạn này bác ghi hơi nhầm 1 xíu e nghĩ là : 2^4 , 2^3 và 2^2.

Mình nhìn là thấy mà. Nếu bạn không thấy thì giải tay chuyển sang float nhé.

0.2 = 0.(0011)_2 = 1.(1001)_2 \times 2^{-3}
0.4 = 0.0(1100)_2 = 1.(1001)_2 \times 2^{-2}

2 số này dương nên bit dấu có giá trị = 0.

Exponent của 0.2 là 127 + (-3) = 124.
Exponent của 0.4 là 127 + (-2) = 123.

Fraction của 0.2 là 1001'1001'1001'1001'1001'100 (nếu cắt đúng 23 bit).
Fraction của 0.4 là 1001'1001'1001'1001'1001'100 (nếu cắt đúng 23 bit).

Còn nếu làm tròn thì fraction của 0.2 và 0.4 sẽ tận cùng = 1.

Viết rồi thấy ở trên nhầm dấu + thành -

dog_unamused

0.1, 0.2, 0.3 < 1 thì số mũ đằng sau là 2^2, 2^3, 2^4 > 1 làm sao được bạn? 1 phẩy nhân với 4, 8, 16 mà ra 0.1 hả?

2 Likes

ok bác em hơi nhầm xíu cái nhân thêm để chuyển dấu .

khả năng e đã hiểu ý bác rồi.
0.2 +0.2=2*0.2 tức là cộng thêm 1 đơn vị vào số mũ của thằng công thức ảnh này, theo như bác vừa biểu diễn thì cái phần fraction nó giống nhau, cho nên 0.2+0.2=0.4.

với 0.2+0.2=0.4 nó có thể suy nghĩ như thế, nhưng với 0.3+0.4=0.7
thằng 0.3, 0.4, 0.7 nó cũng vô hạn ở nhị phân, liệu bác có giải pháp nào để tính nhanh xem liệu chúng có bằng nhau ko ạ? chứ e coi cách cộng 2 float point nó lằng nhằng lắm, e cũng chưa hiểu cách cộng.

Vẫn là từ khoá mà cụ @tntxtnt có đưa, mình đọc 1 link lý thuyết

đọc thêm SSE4 Reference

và Phụ lục D, What Every Computer Scientist Should Know About Floating-Point Arithmetic, by David Goldberg:

https://docs.oracle.com/cd/E19957-01/806-3568/ncg_goldberg.html

Vấn đề của 1 bộ cộng floating point là rounding của bộ cộng đó là gì. Có 4 rounding mode (theo IEEE 754):

  • Round to the Nearest Value
  • Round toward Zero (truncate value)
  • Round toward Positive Infinity
  • Round toward Negative Infinity
3 Likes

cắt còn có làm tròn 1001 thành 101 nha :V :V

2 Likes

Em có nói vậy mà cụ :pouting_cat:

2 Likes
83% thành viên diễn đàn không hỏi bài tập, còn bạn thì sao?