Bên trong chip Intel 8087: Cách phép cộng và trừ số thực dấu phẩy động hoạt động
Bài viết mổ xẻ vi mạch của chip đồng xử lý toán học Intel 8087 (ra mắt năm 1980) để giải thích chi tiết cách phép cộng và trừ số thực dấu phẩy động được thực hiện ở mức vi mã và mạch logic. Qua đó, ta thấy được những thuật toán tinh vi, các trường hợp đặc biệt với vô cực, số không âm/dương và NaN — nền tảng dẫn tới chuẩn IEEE-754 ngày nay.

Năm 1980, Intel giới thiệu chip 8087 — bộ đồng xử lý toán học giúp các phép toán dấu phẩy động trên máy IBM PC và nhiều hệ thống khác trở nên nhanh hơn rất nhiều. Dù 8087 hỗ trợ cả những phép toán phức tạp như tang, căn bậc hai hay logarit, bài viết này tập trung vào hai phép tính cơ bản nhất: cộng và trừ.
So với bộ xử lý 8086, 8087 mang lại tốc độ ấn tượng — phép cộng nhanh hơn gần 100 lần, còn các phép toán phức tạp thì còn nhanh hơn nữa. Để hiểu chính xác cách cộng và trừ dấu phẩy động được hiện thực, tác giả đã bóc nắp một con chip, chụp ảnh độ phân giải cao bằng kính hiển vi và phân tích cả vi mã lẫn mạch điện bên trong.
Sơ đồ đường dữ liệu của chip 8087
Vì sao cần đến số dấu phẩy động?
Hầu hết máy tính dùng số nguyên làm biểu diễn cơ bản. Nhưng nhiều ứng dụng cần đến những con số có phần lẻ, ví dụ 1,23. Hơn nữa, số nguyên có phạm vi rất hẹp: một số nguyên 32 bit chỉ biểu diễn được từ khoảng -2 tỷ đến 2 tỷ.
Số dấu phẩy động giải quyết bài toán này bằng cách biểu diễn một giá trị dưới dạng dấu × phần định trị × 2^số mũ. Phần định trị là một số nhị phân dạng 1.bbb… (một bit 1 đứng đầu, theo sau là dấu chấm nhị phân và phần còn lại). Nhờ số mũ, con số có thể vươn ra khỏi phạm vi hạn chế của phần định trị — thậm chí một số dấu phẩy động ngắn 32 bit cũng phủ được dải từ 10⁻³⁶ đến 10³⁸.
Cái giá phải trả là phép cộng và trừ dấu phẩy động phức tạp hơn nhiều so với số nguyên. Trước khi cộng, hai số phải được căn thẳng dấu chấm nhị phân (giống như căn thẳng dấu phẩy thập phân). Sau phép tính, số mũ của kết quả phải được điều chỉnh, tức chuẩn hóa phần định trị để nó bắt đầu bằng bit 1.
Ví dụ trực quan
Với phép cộng 13,5 + 3,5, hai số được biểu diễn bằng số mũ và phần định trị. Do số mũ lệch nhau 2 đơn vị, số thứ hai phải dịch phải 2 vị trí để khớp. Sau khi cộng phần định trị đã căn thẳng, kết quả bị tràn một bit "thừa" ở bên trái, buộc phải dịch phải 1 vị trí và tăng số mũ lên 1.
Phép trừ thì phức tạp hơn một chút. Với 8 − 7,5, kết quả có thể dương, âm hoặc bằng không, và có thể nhỏ hơn các toán hạng nhiều bậc độ lớn. Sau khi trừ, việc chuẩn hóa có thể cần dịch trái một quãng dài — ví dụ dịch 4 bit và giảm số mũ đi 4. Nếu kết quả âm, phần định trị được chuyển thành dương và dấu kết quả bị đảo. Nếu kết quả bằng không, không thể chuẩn hóa mà phải trả về giá trị đặc biệt +0 hoặc -0.
Kiến trúc bên trong 8087
Bên trong, 8087 lưu số ở định dạng 80 bit gọi là "temporary real", gồm 1 bit dấu, 15 bit số mũ và 64 bit phần định trị. Số mũ 15 bit cho phép biểu diễn những con số khổng lồ trên 10⁴⁹³², còn phần định trị 64 bit cho độ chính xác cực cao. Số mũ được lưu kèm một độ lệch (bias) 16383 để luôn dương.
Sơ đồ thanh ghi của 8087
Chip có 8 thanh ghi nội bộ tổ chức thành một ngăn xếp, mỗi thanh ghi giữ một số dấu phẩy động 80 bit. Để tối ưu hiệu năng, mỗi giá trị còn có một thẻ (tag) đi kèm: zero, valid, special hoặc empty. Thẻ này giúp phát hiện tràn/thiếu ngăn xếp. Ngoài ra còn có các thanh ghi tạm nội bộ tmpA và tmpB (80 bit), cùng tmpC — một thanh ghi đặc biệt không có dấu, số mũ hay thẻ, phần định trị 68 bit gồm thêm 1 bit chống tràn và 3 bit làm tròn gọi là guard, round, sticky.
Trái tim của 8087 là bộ cộng số nguyên 69 bit. Phép cộng số nguyên tốn hai vi lệnh: một lệnh đưa toán hạng thứ nhất vào và chọn tùy chọn (nhớ, làm tròn); toán hạng thứ hai luôn lấy từ thanh ghi B. Kết quả được đặt vào thanh ghi "sum". Phép dịch bit cũng tốn hai vi lệnh và chiếm nhiều diện tích trên đế chip vì cho phép dịch nhanh một giá trị đi số bit bất kỳ.
Vi mã của phép cộng/trừ
8087 hiện thực gần như toàn bộ phép toán bằng vi mã. Bốn phép tính cơ bản (cộng, trừ, nhân, chia) khởi đầu bằng cùng một đoạn vi mã, đưa hai toán hạng vào tmpA và tmpB, rồi gọi một thường trình con riêng theo loại phép toán. Thường trình cộng/trừ bắt đầu ở địa chỉ #0232.
Lưu đồ vi mã phép cộng/trừ
Đoạn vi mã xử lý trước các trường hợp 0+0 hoặc 0−0. Tiếp theo nó trừ số mũ của hai toán hạng; nếu toán hạng thứ nhất có số mũ nhỏ hơn, hai toán hạng được hoán đổi để số lớn hơn đứng trước. Dấu của kết quả được lưu trong một chốt dấu và bị đảo nếu việc hoán đổi làm thay đổi dấu.
Điểm mấu chốt nằm ở địa chỉ #0250: phép cộng hay trừ ở mức cao không nhất thiết khớp với phép toán không dấu ở mức thấp. Ví dụ, với 2+3 hay −2−3, hai giá trị tuyệt đối được cộng; nhưng với −2+3 hay 3−2, hai giá trị tuyệt đối lại được trừ. Chip có mạch chuyên dụng để xác định điều này, dựa trên biểu thức dấu của tmpA, dấu của tmpB và mã phép toán.
Nhánh cộng không dấu tương đối đơn giản: căn thẳng hai số, cộng chúng, rồi chuẩn hóa. Nếu có nhớ từ bit cao nhất, kết quả dịch phải 1 bit và bộ đếm điều chỉnh số mũ tăng thêm. Cuối cùng số mũ được tính từ số mũ ban đầu, cộng các điều chỉnh và trừ 1, rồi đưa qua "bộ chuyển đổi số mũ" để phát hiện tràn hoặc dưới ngưỡng.
Nhánh trừ không dấu dùng bộ cộng 64 bit để thực hiện tmpA − tmpB (đảo toán hạng thứ hai). Nếu kết quả bằng không, chip kiểm tra các bit làm tròn để quyết định xem nên giữ +0 hay -0. Các số không có dấu theo hành vi "nghiêng một phía" quen thuộc: ⁺0 + ⁺0 cho ⁺0, ⁻0 − ⁺0 cho ⁻0; nhưng khi hai số không triệt tiêu nhau, chế độ làm tròn xuống cho ⁻0 còn các chế độ khác cho ⁺0.
Mạch đếm số không đứng đầu
Khi trừ, các chữ số có thể triệt tiêu lẫn nhau, để lại những số 0 ở đầu cần được loại bỏ bằng cách dịch. Mạch đếm số không đứng đầu làm nhiệm vụ đếm có bao nhiêu số 0 nằm bên trái bit 1 đầu tiên và chuyển thành giá trị nhị phân.
Mạch phát hiện 0 đến 7 bit 0 đứng đầu
Thay vì dựng một bộ phát hiện 64 bit phức tạp, 8087 chọn cách đơn giản hơn: một lượt loại bỏ số 0 theo từng byte, rồi một lượt loại bỏ 0 đến 7 bit 0 còn lại. Mỗi lượt chỉ cần một bộ phát hiện 8 đầu vào — chậm hơn, nhưng mạch gọn hơn nhiều.
Các trường hợp đặc biệt với vô cực
8087 xử lý rất cẩn thận các trường hợp đặc biệt liên quan đến vô cực, số không, NaN và số không chuẩn tắc (denormal). Đoạn vi mã chung cho cả bốn phép tính cơ bản giống như "mì spaghetti" với 18 nhánh điều kiện liên tiếp.
Có 8 đường dẫn trả về NaN cho các phép vô nghĩa như n÷denormal, 0÷0, ∞×0, ∞÷∞ hay ∞−∞. Chip có hai chế độ vô cực: "affine closure" (vô cực có dấu, nên ∞+∞ hợp lệ nhưng ∞−∞ vô nghĩa) và "projective closure" (vô cực không dấu, được khuyến nghị).
Có 8 đường dẫn trả về vô cực, ví dụ n÷0, ∞×n, ∞÷∞ hay x±∞. Với nhân và chia, dấu vô cực được tạo từ việc kết hợp dấu các toán hạng; với cộng và trừ, dấu lấy từ chính toán hạng vô cực — nói cách khác, nhân/chia có thể lật dấu vô cực nhưng cộng/trừ thì không.
Kết luận
Hầu hết mọi người không mấy bận tâm đến những phép toán như cộng hay trừ trên máy tính. Nhưng đằng sau hậu trường là cả một hệ thuật toán thú vị cùng vô số trường hợp đặc biệt, được hiện thực bằng những mạch silicon tinh xảo. Những quy tắc phức tạp xử lý số không âm/dương, vô cực âm/dương và NaN đã trở thành một phần của máy tính hiện đại, kể từ khi 8087 dẫn tới chuẩn IEEE-754 mà ngày nay vẫn được dùng.
Có điều, 8087 hơi "quá tay" với các trường hợp đặc biệt. Một số tính năng như projective infinity, số "unnormal" và "pseudo-zero" về sau đã bị loại khỏi chuẩn vì không đáng công sức. Với những ai quan tâm đến lịch sử phần cứng — và đặc biệt là giới lập trình viên nhúng, người hâm mộ kiến trúc x86 hay sinh viên kiến trúc máy tính — câu chuyện về 8087 vẫn là một bài học quý về cách những tính toán tưởng chừng đơn giản nhất lại ẩn chứa bao tầng logic tinh vi bên dưới.