Đã bao giờ bạn dùng javascript và tự hỏi tại sao 0.1 + 0.2 lại bằng 0.30000000000000004 chưa? Đó là bởi vì trong máy tính, số thập phân sẽ được biểu diễn một cách gần đúng với giá trị mà bạn mong muốn, và chúng ta sẽ tìm hiểu về nó trong bài này.
Nội dung bài viết:
- Biểu diễn một số thập phân dưới dạng floating point trong máy tính.
- Chuyển floating point ngược lại số thập phân.
- Cộng hai số được biểu diễn dưới dạng floating point.
- Tại sao chúng ta lại biểu diễn số thập phân dưới dạng floating point, mà không phải là một dạng khác?
0.
Floating point 32 format. (image from Wikipedia) |
Trước khi vào bài viết, bạn cần biết rằng hiện tại có hai loại floating point được dùng phổ biến. Loại thứ nhất gọi là Single-precision floating point, sử dụng 32 bit để biểu diễn số thập phân. Loại thứ hai là Double-precision floating point, sử dụng 64 bit để biểu diễn số thập phân, do dùng nhiều bit hơn nên nó có thể biểu diễn số thập phân lớn hơn, với độ chính xác cao hơn. Trong bài này chúng ta chỉ xem xét những ví dụ với single precision cho ngắn gọn.
1. Biểu diễn một số thập phân dưới dạng floating point
-2.2
ở đây chúng ta sẽ không cần quan tâm tới dấu của phần nguyên vội, nên chúng ta sẽ chuyển 2 sang hệ nhị phân trước:
(2)10 -> (10)2
phần thập phân ở đây là 0.2, để chuyển 0.2 thành phần thập phân chúng ta sẽ làm như sau:
0.2 * 2 = 0.4 => 0 chúng ta lấy số 0 của 0.40.4 * 2 = 0.8 => 0 mang 0.4 ở bên trên xuống, sau đó lấy số 0 của 0.80.8 * 2 = 1.6 => 1 mang 0.8 ở bên trên xuống, sau đó lấy số 1 của 1.80.6 * 2 = 1.2 => 1 mang phần thập phân 0.6 của 1.6 xuống, sau đó lấy 1 của 1.2
--- các phép tính sẽ bị lặp lại như dưới mãi
0.2 * 2 = 0.4 => 00.4 * 2 = 0.8 => 00.8 * 2 = 1.6 => 10.6 * 2 = 1.2 => 1
---
0.2 * 2 = 0.4 => 0.... cứ tiếp tục lặp đến vô hạn, nhưng sau đó, chúng ta chúng ta chỉ giữ lại vừa đủ thôi.
Chúng ta có thể biểu diễn 0.2 dưới dạng nhị phân như sau: 0011 0011 0011 0011 ........ và cứ 0011 mãi
(10.0011 0011 0011 0011 0011 0011 .....)2
1002 => 1.002 * 103123.4 => 1.234 * 1020.002 => 2 * 10-3
Bước này chúng ta sẽ dịch chuyển dâu chấm sang bên trái của số-1-ngoài-cùng-bên-trái. Trong ví dụ này, chúng ta chuyển dấu chấm về bên trái 1 vị trí. Do chúng ta đang xài hệ nhị phân nên nhân với 2x nhé.
10.0011 0011 0011 => 1.0 0011 0011 0011 * 21
Phần được bôi màu xanh lá này được gọi là mantissa.Phần được bôi màu xanh dương gọi là exponent, và nó có thể mang giá trị âm.
x xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxxSign bit Bias exponent Mantissa
Sign bit: gồm 1 bit, là bit dấu , trong ví dụ này -2.2 là âm, nên bit này sẽ có giá trị là 1.
Mantissa: gồm 23 bit, dùng để biểu diễn phần mantissa được tính ở bước 4, (0 0011 0011 0011 0011 0011 00)2
Bias exponent: gồm 8 bit, dùng để biểu diễn số mũ (exponent) mà ta tìm được ở bước 4, trong ví dụ này số mũ là 1.Nhưng vì số mũ cũng có thể âm, nên để có thể biểu diễn số mũ ở đây hơi lằng nhằng một tí. Chúng ta sẽ không chuyển số 1 thành nhị phân ngay mà phải cộng thêm một lượng gọi là bias vào trước.Bias = 2(k-1)-1, k = số bit tối đa của bias exponent, trong ví dụ này là 8.Trong ví dụ ngày, bias = 2(8-1)-1 = 127Bias exponent = bias + exponent = 127 + 1 = (128)10 => (1000 0000)2
Đến bước này chúng ta sẽ thu được dạng floating point của -2.2 dưới dang nhị phân như sau:
1 10000000 00011001100110011001101
Tất nhiên theo cách biểu diễn này thì gía trị mà floating point biểu diễn chỉ gần bằng -2.02 thôi, chính xác là -2.019999980926513671875 trong ví dụ này. Lát nữa chúng ta sẽ tìm hiểu về cách chuyển floating ngược lại số thập phân.
0.35 * 2 = 0.7 => 0
0.7 * 2 = 1.4 => 1
0.4 * 2 = 0.8 => 0
0.8 * 2 = 1.6 => 1
0.6 * 2 = 1.2 => 1-----0.2 * 2 = 0.4 => 00.4 * 2 = 0.8 => 0
0.8 * 2 = 1.6 => 1
0.6 * 2 = 1.2 => 1
---0.2 * 2 = 0.4 => 00.4 * 2 = 0.8 => 0
0.8 * 2 = 1.6 => 1
0.6 * 2 = 1.2 => 1---0.2 * 2 = 0.4 => 0..... cứ tiếp tục lặp đến vô hạn.Biểu diễn 0.35 dưới dạng nhị phân như sau: (01011 0011 0011 0011 0011 ...)2
(0.01011 0011 0011 0011)2
Ở bước này, chúng ta sẽ chuyển dấu chấm về bên trái của số-1-ngoài-cùng-bên-trái. Trong ví dụ này, chúng ta chuyển dấu chấm sang bên phải 2 vị trí.
0.01011 0011 0011 0011 => 001.011 0011 0011 0011 * 2-2
Sau bước này, chúng ta có được mantissa và exponent.
Bias = 2 (8 - 1) - 1 = 127
Bias exponent = bias + exponent = 127 + (-2) = (125)10 => (1111 1010)2
1 1111 1010 011 0011 0011 0011 0011
Vì -0.35 nên sign bit là 1. Bây giờ chúng ta đã có được biểu diễn floating point của -0.35.
2. Chuyển floating point ngược lại số thập phân.
Xem xét ví dụ về floating point 1 10000001 0100 0000 0111 0010 1011 000:
1 10000001 0100 0000 0111 0010 1011 000Sign bit Bias exponent Mantissa
Sign bit: bằng 1, có nghĩa đây là số âm
Bias exponent: (10000001)2 => (129)10
Bias = 2 (8 - 1) - 1 = 127
Exponent: bias exponent - bias = 2
Vậy số thập phân được biểu diễn dưới dạng scientific notation và hệ nhị phân như sau:
(1.0100 0000 0111 0010 1011 000 * 22)2
Dịch chuyển dấu chấm thập phân sang bên phải để bỏ scientific notation:
(1.0100 0000 0111 0010 1011 000 * 22)2 => (101.00 0000 0111 0010 1011 000)2
Bây giờ chúng ta thu được số thập phân dưới dạng hệ nhị phân như sau:
(101.00 0000 0111 0010 1011 000)2
Chuyển phần nguyên về hệ thập phân:
(101)2 => (5)10
Chuyển phần sau dấu chấm về hệ thập phân:
(0.00 0000 0111 0010 1011 000)2 => (0.006999969482421875)10
Ví dụ: (0.011010)2 = (0 × 2⁰) + (0 × 2⁻¹) + (1 × 2⁻²) + (1 × 2⁻³) + (0 × 2⁻⁴) + (1 × 2⁻⁵) + (0 × 2⁻⁶) = (0.40625)₁₀Cộng phần nguyên vào và thêm dấu, cuối cùng chúng ta thu được:
(-5.006999969482421875)10
Gần bằng 5.007 - là số mà mình muốn biểu diễn.
3. Cộng hai số được biểu diễn dưới dạng floating point
(0.1)10=> (0 01111111011 1001100110011001100110011001100110011001100110011010)FP=> (1.100110011001100110011001100110011001100110011001101 * 2-4)2=> (0.0001100110011001100110011001100110011001100110011001101)2
(0.2)10=> (0 01111111100 1001100110011001100110011001100110011001100110011010)FP=> (1.100110011001100110011001100110011001100110011001101 * 2-3)2=> (0.001100110011001100110011001100110011001100110011001101)2
(0.0001100110011001100110011001100110011001100110011001101)2+(0.001100110011001100110011001100110011001100110011001101)2=(0.0100110011001100110011001100110011001100110011001101000)2=(0.3000000000000000444089209850062616169452667236328125)10
4. Tại sao chúng ta lại dùng floating point?
Bên cạnh floating point, thì chúng ta còn một cách khác là fixed point. Ví dụ fixed point 32 bit sẽ có format như sau:
x xxxx xxxx xxxx xxx xxxx xxxx xxxx xxxxSign bit Integral part Fraction part
Sign bit: gồm 1 bit để chứa dấu của số cần biểu diễn, số âm tương ứng với bit 1
Integral part: gồm 15 bit, chứa phần nguyên của số thập phân
Fraction part: gồm 16 bit, chứa phần thập phân sau dấu chấm
Đối với kiểu fixed point này thì khoảng giá trị mà nó biểu diễn được khá nhỏ khi so với floating point.
Số nguyên dương khác không nhỏ nhất mà nó biểu diễn được là:
(2-16)2 = (0.00001525878)10
Số nguyên dương lớn nhất mà fixed point có thể biểu diễn là:
(215-1)2+ (2-16)2 = (32767.00001525878)10
Trong khi đó, số lớn nhất mà floating point có thể biểu diễn là:
(2 − 2−23) × 2127 ≈ 3.4028235 × 1038
Chúng ta có thể thấy floating point biểu diễn được một khoảng giá trị lớn hơn nhiều so với fixed point. Nhưng đồng thời chúng ta phải hi sinh đi sự chính xác và thời gian - vì cần nhiều thời gian để xử lí floating point hơn.
5. Mẹo khi làm việc với floating point
if (0.1 + 0.2 === 0.3) {// do something}
const eps = 0.001if (Math.abs((0.1 + 0.2) - 0.3) < eps) {// do something}
Comments
Post a Comment
» Vui lòng không spam vì nó sẽ bị xóa ngay sau đó.
» Nếu chèn code hãy mã hóa trước khi chèn vào nhận xét.
» Nếu thủ thuật Blog không áp dụng được thì hãy để lại URL blog để mình tiện kiểm tra.