18/11/2022

CÁCH MÁY TÍNH LƯU SỐ THỰC TRONG BỘ NHỚ VÀ SAI SỐ CỦA NÓ

 

Nếu bạn nghĩ máy tính luôn đúng, hãy thử câu lệnh sau

print( 0.1 + 0.2 + 0.3 == 0.6 )

Python sẽ trả về kết quả False?

Tại sao lại có sự tính toán sai lầm này của máy tính. Để trả lời câu hỏi, ta cần tìm hiểu về cách máy tính lưu trữ số thực (float number) trong bộ nhớ và tại sao lại có sự sai số như vậy.

Số lượng bit dùng để lưu 1 số là cố định với mọi máy tính, thường là 64 bit. Các số được lưu trữ bởi 3 phần: phần xác định dấu s(sign indicator), để xác định số đó là âm hay dương; phần số mũ e(exponent), là lũy thừa của 2 , phần hệ số f(fraction), hệ số của luỹ thừa bậc 2. Ví dụ nếu phần s, e, f của một số là 1, 10, 11 thì số đó là (-1)^1 * 2^2 * 3 = -12, hoặc nếu s,e,f lần lượt là 0, 11, 101 thì số đó là (-1)^0 * 2^3 * 5 = 40

 Hầu như tất cả các hệ thống máy tính lưu float number trong Python bằng chuẩn IEEE754 – bao gồm 64 bit. 1 bit được phân bổ cho phần xác định dấu, 11 bit được phân bổ cho phần số mũ và 52 bit được phân bổ cho phần hệ số. Với 11 bit được phân bổ cho số mũ, điều này tạo ra 2048 giá trị mà số mũ có thể nhận. Vì chúng ta muốn có thể tạo các số có độ rất chính xác cao nên chúng tôi muốn một số giá trị này biểu thị số mũ âm (để có thể biểu diễn các số nằm trong khoảng từ 0 đến 1 trong hệ cơ số 10). Để thực hiện điều này, 1023 được trừ khỏi số mũ để chuẩn hóa nó, ( ví dụ, giá trị phần số mũ là 5 đại diện cho giá trị 2^(5-1023) = 2^-1018 ). Phần hệ số là một số nằm trong khoảng từ 1 đến 2. Trong hệ nhị phân, điều này có nghĩa là số hạng đứng đầu sẽ luôn là 1 và do đó, sẽ rất lãng phí bit để lưu trữ nó. Để tiết kiệm không gian, số 1 hàng đầu bị loại bỏ. Do đó dạng chung của float là (-1)^s * 2^(e-1023) * (1+f), với s, e, f là phần xác định dấu, số mũ và hệ số.

 Trong Python, chúng ta có thể lấy thông tin float number bằng thư viện sys như bên dưới:

import sys

sys.float_info


Các số thập phân được lưu dưới dạng chung:

n = (-1)^s * 2^(e-1023) * (1+f)

Câu hỏi: Số 1 10000000010 1000000000000000000000000000000000000000000000000000 (IEEE754) là số nào trong hệ cơ số 10

Ta có phần xác định dấu là 1, đại diện cho số dương; phần số mũ là 10000000010, đại diện cho 1 * 2^10 + 1 * 2^1 = 1026, phần cơ số là 1 * 1/ 2^1 + 0 * 1/ 2^2 + 0 * 1/ 2^3 + … = 0.5 Do đó từ dạng chung ta có

n = (-1)^1 * 2^3 * (1+0.5) = -12

Bằng việc lưu trữ số bằng chuẩn IEEE754, số lớn nhất máy tính có thể lưu trữ được là

0 11111111110 1111111111111111111111111111111111111111111111111111 (IEEE754), khoảng 1.79 * 10^308 trong cơ số 10, và số dương nhỏ nhất có thể lưu được là 0 00000000001 000000000000000000000000000000000000000000000000000 (IEEE754), bằng khoảng 5 * 2^-324, và mọi số nhỏ hơn số này được máy tính coi là bằng 0.

Tuy nhiên chúng ta có thể thấy việc lưu trữ số bằng chuẩn IEEE754 khiến máy tính không thể lưu trữ chính xác được giá trị của mọi số thực(do trên lí thuyết thì số các số khác nhau trong chuẩn IEEE754 là rất lớn nhưng vẫn hữu hạn), mà chỉ lưu một giá trị xấp xỉ của số đó. Để dễ hình dung hơn, ta sẽ xét ví dụ sau

Trong chuẩn IEEE754 số 15 được lưu dưới dạng 0 10000000010 1110000000000000000000000000000000000000000000000000, số liền kề nhỏ hơn nó là 0 10000000010 1101111111111111111111111111111111111111111111111111 = 14.9999999999999982236431605997, và số liền kề lớn hơn nó là 0 10000000010 1110000000000000000000000000000000000000000000000001 = 15.0000000000000017763568394003                                      

Do đó, với mỗi số thực nằm ở trong khoảng trên đều được biểu diễn bởi một trong ba số này trong chuẩn IEEE754, cũng có nghĩ khi tính toán, mọi kết quả nằm trong khoảng này đều được làm tròn về giá trị chuẩn IEEE754 gần nhất, chính điều này đã tạo ra sự sai lệch trong tính toán ở câu hỏi đưa ra ở phần đầu bài viết. Khi đó, trong khi tính toán 0.1 + 0.2 + 0.3 máy tính đã xấp xỉ kết quả này bằng một giá trị IEEE754 rất gần 0.6(nhưng không cùng giá trị IEEE754 với số 0.6), nên khi in ra kết quả của 0.1 + 0.2 + 0.3 == 0.6 sẽ là False.