Bỏ qua tới nội dung
LẬP TRÌNHGIẢI THÍCH

Vì sao máy tính tính 0,1 + 0,2 không ra 0,3? Bí mật của số thực

Gõ 0.1 + 0.2 vào máy, kết quả là 0.30000000000000004. Không phải lỗi phần mềm: đó là cách máy tính lưu số thập phân bằng hệ nhị phân, và nó từng khiến tên lửa Patriot trượt mục tiêu.

BBeernary01/11/2026 · 12 PHÚT ĐỌC
COVER.BMP
Vì sao máy tính tính 0,1 + 0,2 không ra 0,3? Bí mật của số thực
▲ Máy tính lưu số thập phân bằng hệ nhị phân nên 0,1 luôn chỉ là số xấp xỉ

Bạn mở bảng điều khiển của trình duyệt, gõ 0.1 + 0.2 rồi nhấn Enter, chờ đợi một con số 0.3 tròn trịa. Màn hình lại trả về 0.30000000000000004. Cái máy tính tính được quỹ đạo vệ tinh, dựng phim 3D, mà lại cộng sai phép toán lớp 4?

Thật ra máy không cộng sai. Nó chỉ đang nói một "ngôn ngữ số" khác với chúng ta, và trong ngôn ngữ đó, 0,1 là một con số không bao giờ viết trọn được. Bài này đi từ con chip bên trong máy cho tới cách ngân hàng giữ từng đồng tiền của bạn, để bạn hiểu vì sao chuyện này xảy ra, khi nào nó vô hại, khi nào nó nguy hiểm, và lập trình viên xử lý ra sao.

Có phải phần mềm bị lỗi không?

Phản xạ đầu tiên của nhiều người là "chắc JavaScript dở". Nhưng thử sang ngôn ngữ khác, kết quả y hệt:

python
>>> 0.1 + 0.2
0.30000000000000004
>>> 0.1 + 0.2 == 0.3
False

Python, JavaScript, Java, C, C#, Go đều ra cùng con số đó, tới từng chữ số cuối. Excel thì khéo hơn: nó chỉ hiển thị 15 chữ số có nghĩa nên bạn thấy 0,3. Nhưng gõ công thức =1*(0.5-0.4-0.1) vào một ô, Excel trả về một số cực nhỏ khác 0 (cỡ -2,8 × 10⁻¹⁷) chứ không phải 0. Sai số vẫn nằm đó, chỉ là bị giấu đi.

Bàn phím máy tính bỏ túi đời cũ với phím DECIMAL POINT to bản cạnh dãy số

Khi mọi ngôn ngữ, mọi hãng đều "sai" giống hệt nhau, thì đó không phải lỗi của ai cả. Đó là hệ quả của một quy ước chung: chuẩn IEEE 754 (chuẩn số thực dấu phẩy động), được ban hành năm 1985 và gần như mọi bộ vi xử lý hiện đại đều tuân theo. Muốn hiểu vì sao chuẩn này cho ra 0,30000000000000004, ta phải đi xuống tận cách máy tính ghi một con số vào bộ nhớ.

Máy tính đếm bằng hai ngón tay

Bên trong con chip là hàng tỷ transistor (bóng bán dẫn), mỗi cái chỉ có hai trạng thái: dẫn điện hoặc không. Hai trạng thái đó được gọi là 1 và 0. Vì vậy máy tính đếm bằng hệ nhị phân (cơ số 2), giống như một người chỉ có hai ngón tay, trong khi con người quen đếm bằng mười ngón, tức hệ thập phân (cơ số 10).

Một vi mạch tích hợp đã mở nắp, lõi silicon nhỏ nối với các chân bằng sợi dây vàng mảnh

Với số nguyên, hệ nhị phân không gặp rắc rối gì. Mỗi vị trí là một lũy thừa của 2: 1, 2, 4, 8, 16... Muốn viết số nào, ta chỉ cần bật đúng các vị trí cộng lại ra số đó:

Số thập phân Phân tích Nhị phân
5 4 + 1 101
12 8 + 4 1100
100 64 + 32 + 4 1100100

Mọi số nguyên đều phân tích được như vậy, không thừa không thiếu. Đó là lý do phép cộng, trừ, nhân số nguyên trên máy tính luôn chính xác tuyệt đối (miễn là không vượt quá số bit cho phép).

Hai cuộn băng giấy đục lỗ, mỗi hàng lỗ là một dãy bit 0 và 1 của máy tính đời đầu

Phần lẻ: ghép số từ những mảnh một nửa

Rắc rối bắt đầu ở phía sau dấu phẩy. Trong hệ thập phân, các chữ số sau dấu phẩy lần lượt là phần mười, phần trăm, phần nghìn. Trong hệ nhị phân, chúng là một nửa, một phần tư, một phần tám, một phần mười sáu... mỗi mảnh bằng đúng một nửa mảnh trước.

Hình dung bạn có một bộ quả cân: 0,5 kg, 0,25 kg, 0,125 kg, 0,0625 kg... và chỉ được dùng mỗi loại tối đa một quả. Có những khối lượng cân được ngay:

  • 0,5 = một nửa → nhị phân 0.1
  • 0,25 = một phần tư → 0.01
  • 0,75 = một nửa + một phần tư → 0.11
  • 0,375 = một phần tư + một phần tám → 0.011

Nhưng thử cân 0,1 kg xem. Quả 0,0625 thì nhỏ hơn, thêm quả 0,03125 thì được 0,09375, vẫn thiếu. Thêm tiếp những quả nhỏ hơn, bạn tiến sát dần tới 0,1 nhưng không bao giờ chạm đúng. Viết ra nhị phân, 0,1 là:

text
0.0001100110011001100110011001100110011...

Cụm 0011 lặp lại mãi mãi, không có điểm dừng.

Quy tắc nhận biết nhanh

Một phân số viết được chính xác trong hệ nhị phân khi và chỉ khi mẫu số (sau khi rút gọn) là lũy thừa của 2: 1/2, 3/4, 5/8, 7/16... Còn 0,1 = 1/10, mẫu số có thừa số 5, nên trong hệ nhị phân nó lặp vô hạn. Vì thế 0,1, 0,2, 0,3, 0,7 đều là số xấp xỉ, còn 0,5 hay 0,25 thì chính xác.

Giống hệt chuyện viết 1/3 ra giấy

Nếu thấy lạ, hãy nhớ rằng chính hệ thập phân của chúng ta cũng có nhược điểm y như vậy. Viết 1/3 ra số thập phân, bạn được 0,3333... với số 3 kéo dài vô tận. Tờ giấy có hạn, nên bạn phải dừng ở đâu đó, chẳng hạn 0,3333. Phần bị cắt đi chính là sai số làm tròn. Cộng ba lần 0,3333 bạn được 0,9999 chứ không phải 1.

Thước dây may đo với vạch chia milimet, minh hoạ giới hạn độ chính xác khi đo

Máy tính gặp đúng chuyện đó, chỉ khác là "con số 1/3" của nó lại là 0,1, một số mà con người thấy hết sức tròn trịa. Mỗi hệ đếm có những phân số "khó chịu" riêng: hệ 10 khổ với 1/3, hệ 2 khổ với 1/10. Cũng như thước dây chỉ chia tới milimet, đo vật dài 0,35 mm thì phải ước chừng, bộ nhớ máy tính chỉ có một số ô nhất định để ghi chữ số.

64 bit và nhát cắt cuối cùng

Kiểu số thực phổ biến nhất hiện nay là double (độ chính xác kép) theo chuẩn IEEE 754, dùng đúng 64 bit cho mỗi con số, chia làm ba phần:

Phần Số bit Vai trò
Dấu (sign) 1 Số âm hay dương
Số mũ (exponent) 11 Dời dấu phẩy sang trái hay phải bao nhiêu bậc
Phần định trị (fraction) 52 Các chữ số có nghĩa của con số

Sơ đồ 64 bit của số thực double: 1 bit dấu, 11 bit số mũ và 52 bit phần định trị

Cách lưu này giống ký hiệu khoa học bạn học ở trường (như 6,02 × 10²³), chỉ là cơ số 2. Nhờ phần số mũ, double biểu diễn được cả số cực nhỏ lẫn cực lớn, nhưng độ chính xác luôn chỉ khoảng 15–17 chữ số thập phân có nghĩa, vì phần định trị chỉ có 52 bit (cộng thêm một bit ngầm định).

Với 0,1, chuỗi 0011 lặp vô hạn bị cắt ở bit thứ 52, rồi làm tròn tới giá trị gần nhất. Kết quả là con số thật sự nằm trong bộ nhớ là:

text
0.1000000000000000055511151231257827021181583404541015625

Lớn hơn 0,1 một chút xíu. Tương tự, 0,2 được lưu thành 0,2000000000000000111..., cũng nhỉnh hơn. Còn 0,3 lại được lưu thành 0,2999999999999999888..., hơi hụt. Khi cộng hai số "hơi dư", tổng bị đẩy lên, rồi làm tròn một lần nữa, rơi vào số double nằm ngay trên 0,3 chứ không phải số mà máy dùng cho 0,3. Hai số đó khác nhau ở bit cuối cùng, và khi in ra đầy đủ, bạn thấy đuôi ...00004.

Tự kiểm tra trong Python

Gõ from decimal import Decimal rồi Decimal(0.1), Python sẽ in ra đúng giá trị thật mà máy đang lưu cho 0,1, dài hơn 50 chữ số. Đây là cách nhanh nhất để "nhìn tận mắt" sai số.

Khi sai số nhỏ xíu gây hậu quả lớn

Sai số ở chữ số thứ 17 thường vô hại, chẳng ai quan tâm. Nó chỉ trở nên nguy hiểm trong hai tình huống.

Thứ nhất: so sánh bằng. Câu lệnh if (0.1 + 0.2 == 0.3) cho kết quả sai. Một chương trình kiểm tra "đã đủ tiền chưa", "đã tới đích chưa" bằng phép so sánh tuyệt đối có thể chạy lệch hướng chỉ vì một bit.

Thứ hai: cộng dồn rất nhiều lần. Mỗi phép tính thêm một chút sai lệch. Lặp hàng triệu lần, những chút đó có thể dồn lại thành con số đáng kể.

Ví dụ nổi tiếng nhất là hệ thống tên lửa Patriot trong Chiến tranh vùng Vịnh. Ngày 25/2/1991 tại Dhahran (Ả Rập Xê Út), một khẩu đội Patriot không đánh chặn được tên lửa Scud bay tới, khiến 28 binh sĩ Mỹ thiệt mạng. Theo báo cáo điều tra của Văn phòng Kiểm toán Chính phủ Mỹ (GAO), hệ thống đếm thời gian theo đơn vị 0,1 giây và lưu con số 0,1 trong một thanh ghi 24 bit. Vì 0,1 không viết trọn được trong hệ nhị phân, mỗi nhịp đếm hụt một chút cực nhỏ. Khẩu đội đã chạy liên tục khoảng 100 giờ, và sai số dồn lại thành khoảng 0,34 giây. Với một tên lửa Scud bay khoảng 1.676 mét mỗi giây, 0,34 giây tương đương hơn nửa kilomet, đủ để radar tìm sai chỗ và bỏ qua mục tiêu.

Vệt sáng của tên lửa Patriot phóng lên đánh chặn Scud trên bầu trời thành phố Israel năm 1991

Bài học từ Patriot

Lỗi không nằm ở một phép tính sai lớn, mà ở một sai số nhỏ được cộng dồn qua hàng triệu lần lặp. Bất kỳ phần mềm nào cộng dồn số thực trong thời gian dài, như đồng hồ, bộ đếm, mô phỏng, đều có thể gặp kiểu lỗi này.

Ngân hàng và lập trình viên xử lý thế nào?

Tin tốt là vấn đề này đã được biết từ lâu, và có những cách xử lý rất gọn.

Tiền tệ: đừng dùng số thực

Ngân hàng, ví điện tử, phần mềm kế toán không lưu tiền bằng double. Có hai cách phổ biến:

  • Đổi ra đơn vị nhỏ nhất và lưu số nguyên. Thay vì lưu 12,50 USD, hệ thống lưu 1250 cent. Số nguyên luôn chính xác, nên cộng trừ bao nhiêu lần cũng không lệch một xu. Với tiền đồng Việt Nam vốn không có phần lẻ, việc này càng tự nhiên.
  • Dùng kiểu số thập phân chuyên dụng như Decimal trong Python, BigDecimal trong Java, decimal trong C#, hay kiểu NUMERIC/DECIMAL trong cơ sở dữ liệu. Các kiểu này tính toán theo hệ 10, nên 0,1 + 0,2 ra đúng 0,3, đổi lại chạy chậm hơn số thực phần cứng.

Xấp tờ tiền polymer 500.000 đồng xếp xoè thành hình tròn

python
>>> from decimal import Decimal
>>> Decimal("0.1") + Decimal("0.2")
Decimal('0.3')

Lưu ý phải truyền vào dạng chuỗi "0.1". Nếu viết Decimal(0.1), Python sẽ chuyển con số double đã bị làm tròn sang, và bạn mang theo luôn sai số.

Tính toán khoa học: so sánh có dung sai

Trong đồ hoạ, vật lý, máy học, số thực là bắt buộc vì nhanh. Khi đó lập trình viên không hỏi "hai số có bằng nhau không", mà hỏi "hai số có đủ gần nhau không". Họ lấy hiệu tuyệt đối của hai số rồi so với một ngưỡng rất nhỏ gọi là epsilon:

javascript
Math.abs((0.1 + 0.2) - 0.3) < Number.EPSILON  // true

Number.EPSILON trong JavaScript bằng 2⁻⁵², khoảng 2,22 × 10⁻¹⁶, là khoảng cách giữa 1 và số double kế tiếp. Python có sẵn hàm math.isclose(a, b) làm đúng việc này với dung sai tương đối, an toàn hơn khi số lớn.

Lập trình viên gõ code trên laptop đặt trên bàn gỗ

Một số mẹo thực tế khác:

  • [ ]Không dùng == để so sánh hai số thực; dùng math.isclose hoặc so với epsilon
  • [ ]Lưu tiền bằng số nguyên (đơn vị nhỏ nhất) hoặc kiểu Decimal
  • [ ]Bộ đếm thời gian dài hạn nên đếm bằng số nguyên (số nhịp, số mili giây) rồi mới nhân ra giây
  • [ ]Chỉ làm tròn khi hiển thị cho người dùng, không làm tròn giữa chừng phép tính

Máy tính không tính sai. Nó chỉ chính xác tuyệt đối với những con số ghép được từ lũy thừa của 2, còn lại đều là xấp xỉ rất, rất gần.

Tóm tắt nhanh

  • Máy tính lưu số bằng hệ nhị phân; số nguyên luôn chính xác, nhưng phần lẻ phải ghép từ các mảnh 1/2, 1/4, 1/8...
  • 0,1 trong hệ nhị phân lặp vô hạn, giống 1/3 trong hệ thập phân, nên buộc phải làm tròn.
  • Kiểu double theo chuẩn IEEE 754 có 52 bit phần định trị, chính xác khoảng 15–17 chữ số; 0,1 + 0,2 ra 0,30000000000000004 là đúng theo chuẩn.
  • Sai số nguy hiểm khi so sánh bằng tuyệt đối hoặc cộng dồn hàng triệu lần, như sự cố Patriot năm 1991.
  • Tiền tệ dùng số nguyên hoặc Decimal; tính toán khoa học so sánh bằng epsilon.

Câu hỏi thường gặp

Máy tính bỏ túi có bị lỗi 0,1 + 0,2 không?

Phần lớn máy tính bỏ túi không bị, vì chúng thường tính theo hệ thập phân mã hoá nhị phân (BCD), lưu từng chữ số 0–9 riêng lẻ. Ứng dụng máy tính trên điện thoại và hệ điều hành cũng thường dùng thư viện số thập phân để kết quả hiển thị đúng như người dùng mong đợi.

Dùng float 32 bit thay vì double có đỡ hơn không?

Không, còn tệ hơn. Float 32 bit chỉ có 23 bit phần định trị, chính xác khoảng 7 chữ số thập phân, nên sai số xuất hiện sớm hơn nhiều. Float được dùng khi cần tiết kiệm bộ nhớ và tốc độ, như đồ hoạ game hay mô hình AI.

Sao không bỏ hẳn số thực, chuyển hết sang Decimal?

Vì số thực double được phần cứng xử lý trực tiếp, nhanh hơn Decimal phần mềm rất nhiều. Đồ hoạ, mô phỏng vật lý, AI cần hàng tỷ phép tính mỗi giây và chấp nhận được sai số ở chữ số thứ 16, nên double vẫn là lựa chọn hợp lý.

Làm sao hiển thị 0,3 thay vì 0,30000000000000004?

Làm tròn khi in ra, ví dụ (0.1 + 0.2).toFixed(2) trong JavaScript hoặc round(0.1 + 0.2, 2) trong Python. Chỉ làm tròn ở bước hiển thị, còn giá trị dùng để tính tiếp thì giữ nguyên hoặc chuyển sang Decimal nếu cần chính xác.

BVIẾT BỞIBeernary

ĐỌC TIẾP_

BÌNH LUẬN (0)

LOGIN.EXE
> ĐĂNG NHẬP ĐỂ BÌNH LUẬNDùng tài khoản Google, không cần tạo mật khẩu. Tên và ảnh đại diện Google sẽ hiện cạnh bình luận.

> CHƯA CÓ BÌNH LUẬN. HÃY LÀ NGƯỜI ĐẦU TIÊN_