Floating point number (IEEE 754)

Đã 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

Ở ví dụ này, chúng ta hãy cùng chuyển số -2.2 sang dạng floating point:
-2.2
Bước 1. chuyển phần nguyên sang hệ nhị phân:

ở đâ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

Bước 2. chuyển phần thập phân sang hệ nhị phân:

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.4
0.4 * 2 = 0.8 => 0 mang 0.4 ở bên trên xuống, sau đó lấy số 0 của 0.8
0.8 * 2 = 1.6 => mang 0.8 ở bên trên xuống, sau đó lấy số 1 của 1.8
0.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 => 0
0.4 * 2 = 0.8 => 0 
0.8 * 2 = 1.6 => 
0.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

Bước 3. Biểu diễn 2.2 dưới dạng nhị phân

(10.0011 0011 0011 0011 0011 0011 .....)2 

Bước 4: Chuyển thành dạng scientific notation
một vài ví dụ về chuyển tự dạng số thập phân sang scientific notation cho các bạn dễ hiểu:

1002 => 1.002 * 103
123.4 => 1.234 * 102
0.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.

Bước 5: Tính bias exponent.
ở bước này thì có lẽ chúng ta cần phải tìm hiều về cấu trúc của floating point 32 bit một chút:

x                      xxxx xxxx                    xxxx xxxx xxxx xxxx xxxx xxx
Sign 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 = 127
Bias exponent = bias + exponent = 127 + 1 = (128)10 => (1000 0000)2

Bước 6: Kết quả.

Đế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.

Bây giờ chúng ta cùng làm thêm một ví dụ về chuyển số thập phân sang dạng floating point nữa, lần này là -0.35.

Bước 1: Chuyển phần nguyên sang hệ nhị phân, trong trường hợp này, phần nguyên là 0 nên không cần chuyển.

Bước 2: Chuyển phần thập phân sang hệ nhị 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 => 0
0.4   * 2 = 0.8 => 0
0.8   * 2 = 1.6 => 1
0.6   * 2 = 1.2 => 1
---
0.2   * 2 = 0.4 => 0
0.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

Bước 3: Chuyển thành dạng có dấu chấm thập phân.

(0.01011 0011 0011 0011)2

Bước 4: Chuyển thành dạng scientific notation

Ở 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 mantissaexponent.

Bước 5: Tính bias exponent.

Bias = 2 (8 - 1) - 1 = 127 
Bias exponent = bias + exponent = 127 + (-2) = (125)10 => (1111 1010)2

Bước 6:

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 000
Sign bit        Bias exponent          Mantissa

Sign bit: bằng 1, có nghĩa đây là số âm

Bias exponent: (10000001)2 => (129)10

Bias(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

Chúng ta hãy cùng cộng thử hai số 0.1 và 0.2 dưới dạng floating point xem nó bằng bao nhiêu nhé, lần này chúng ta dùng loại 64 bit, tương ứng với loại mà Javascript đang dùng:

FP = floating point

Bước 1. Chuyển 0.1 từ hệ thập phân sang nhị phân:

(0.1)10 
=> (0 01111111011 1001100110011001100110011001100110011001100110011010)FP 
=> (1.100110011001100110011001100110011001100110011001101 * 2-4)2 
=> (0.0001100110011001100110011001100110011001100110011001101)2

Bước 2. Chuyển 0.2 từ hệ thập phân sang nhị phân:

(0.2)10 
=> (0 01111111100 1001100110011001100110011001100110011001100110011010)FP 
=> (1.100110011001100110011001100110011001100110011001101 * 2-3)2 
=> (0.001100110011001100110011001100110011001100110011001101)2

Bước 3. Cộng hai số nhị phân lại:

(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 xxxx
Sign 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

1. Nếu bạn cần dùng biến để lưu giá trị tiền, thì dùng số nguyên với đơn vị là Đồng, tránh sử dụng số thập phân với đơn vị là nghìn đồng chẳng hạn. Vì floating point sẽ không mang lại cho bạn giá trị sau dấu chấm thập phân đúng tuyệt đối, và tiền là một giá trị rất quan trọng.

2. Nếu phải so sánh hai floating point với nhau, hãy tránh so sánh hai số đó bằng trực tiếp. Mà hãy định nghĩa ra một lượng sai số cho phép, nếu sai số vẫn nằm trong khoảng đó thì coi như hai số bằng nhau.

ví dụ như phép so sánh huyền thoại trong Javascript:

if (0.1 + 0.2 === 0.3) {
    // do something
}

hãy làm như sau, định nghĩa ra khoảng sai số cho phép, ví dụ bạn muốn dúng đến 3 chữ số thập phân:

const eps = 0.001
if (Math.abs((0.1 + 0.2) - 0.3) < eps) {
    // do something
}

6. Tham khảo

https://en.wikipedia.org/wiki/Single-precision_floating-point_format
https://stackoverflow.com/questions/588004/is-floating-point-math-broken
https://en.wikipedia.org/wiki/IEEE_754

Comments