Máy tính nhị phân

Máy tính nhị phân: cộng, trừ, nhân, chia, AND, OR, XOR, NOT, shift. Hỗ trợ BigNumber, đổi cơ số, giải thích bù 2.

Máy tính nhị phân thực hiện toàn bộ các phép toán mà lập trình viên cần trên số cơ số 2: số học (cộng, trừ, nhân, chia, lũy thừa), logic bitwise (AND, OR, XOR, NOT) và toán tử dịch chuyển (left shift, right shift). Mỗi kết quả được hiển thị đồng thời ở dạng nhị phân, thập phân và hex để bạn có thể kiểm tra bằng tay hoặc dán thẳng vào mã nguồn. Đầu vào chấp nhận bất kỳ trong ba cơ số — hữu ích khi bạn có hằng số hex từ datasheet, giá trị thập phân từ tài liệu kỹ thuật, hoặc literal nhị phân từ bitmap — và số học chính xác tùy ý qua BigNumber.js đồng nghĩa không còn ngưỡng tràn 32-bit hay 53-bit. Ứng dụng thường gặp: debug thao tác bit flag, tính netmask, giải mã thanh ghi phần cứng, tính giá trị bù 2, và học cách máy tính thực sự tính toán.

Nhị phân là gì?

Nhị phân là hệ đếm mà máy tính thực sự sử dụng. Nó có đúng hai ký hiệu, 0 và 1, vì phần cứng vật lý — transistor, vùng từ tính, điểm quang học — có thể biểu diễn đáng tin cậy hai trạng thái khác biệt (tắt/bật, điện áp thấp/cao) và gần như không gì khác đạt được tốc độ và độ tin cậy tương đương. Mọi cấu trúc cấp cao hơn trong điện toán — văn bản, hình ảnh, âm thanh, gói tin mạng, chỉ thị — ở mức thấp nhất chỉ là bit.

Mỗi chữ số nhị phân gọi là bit. Tám bit tạo thành một byte (đơn vị nhỏ nhất có địa chỉ riêng trên hầu hết máy tính hiện đại); 16 bit là half-word; 32 hoặc 64 bit là word, khớp với độ rộng thanh ghi gốc của CPU. Trong một byte, các bit được đánh số từ 0 (ít quan trọng nhất, ngoài cùng bên phải) đến 7 (quan trọng nhất, ngoài cùng bên trái). Bit ở vị trí k đóng góp 2^k vào giá trị khi được bật.

Nhị phân là hệ vị trí giống thập phân, nhưng với cơ số 2 thay vì 10. Số nhị phân 1010 khai triển thành 1×2³ + 0×2² + 1×2¹ + 0×2⁰ = 8 + 0 + 2 + 0 = 10 trong thập phân. Chữ số đầu là quan trọng nhất — cùng quy ước với thập phân. Để chuyển thập phân sang nhị phân, chia liên tục cho 2 và ghi lại số dư từ dưới lên; để chuyển nhị phân sang thập phân, cộng lũy thừa của 2 tại mỗi bit được bật.

Tầm quan trọng của nhị phân không chỉ là lịch sử: mọi nguyên thủy tính toán — số học, địa chỉ, định dạng tệp, giao thức mạng — đều được định nghĩa theo bit và căn chỉnh byte. Hiểu nhị phân trực tiếp là điều cho phép lập trình viên suy luận về tràn số, căn chỉnh, endianness, nén dữ liệu và chi phí thao tác. Ngay cả ngôn ngữ cấp cao ẩn bit cũng phơi bày chúng khi hiệu năng quan trọng (bitset, bloom filter, RoaringBitmap, CRC checksum, hàm hash).

Làm thế nào để tính toán nhị phân?

Số học nhị phân tuân theo cùng thuật toán cột-theo-cột bạn đã học cho thập phân — khác biệt duy nhất là bảng chữ chỉ có hai ký hiệu thay vì mười, nên quy tắc nhớ/mượn chặt chẽ hơn.

  • Phép Cộng Nhị Phân
    • Cùng thuật toán cột với thập phân, nhưng tổng một chữ số lớn nhất là 1 + 1 = 10₂ (bằng 2 trong thập phân, viết 0 ở cột này và nhớ 1 sang trái).
    • Quy tắc một cột: 0+0=0, 0+1=1, 1+0=1, 1+1=10 (nhớ 1). Khi có nhớ vào: 1+1+1=11 (viết 1, nhớ 1).
    • Ví dụ: 1011 (11) + 1101 (13) = 11000 (24). Kiểm tra bằng cách cộng cột từ phải sang với nhớ.
    • Phần cứng cộng nhị phân chính xác theo cách này bằng các bộ cộng đầy đủ nối tầng; CPU hiện đại cộng cặp 64-bit trong một chu kỳ xung nhịp bằng cách tính tất cả nhớ song song (carry-lookahead).
  • Phép Trừ Nhị Phân
    • Quy tắc cột cho phép trừ: 0-0=0, 1-0=1, 1-1=0, 0-1 cần mượn từ cột tiếp theo (cho 1 ở cột này, trừ 1 ở cột bên trái).
    • Bên trong CPU, phép trừ gần như không bao giờ làm bằng mạch mượn riêng. Thay vào đó, a-b được tính như a + (~b + 1) — phép cộng với bù 2 của b — dùng cùng một bộ cộng.
    • Ví dụ: 1011 (11) - 100 (4) = 111 (7). Hoặc qua bù 2: ~100 + 1 trong 4 bit = 1100, rồi 1011 + 1100 = 10111, bỏ bit nhớ ra, còn 0111 = 7.
    • Đó là lý do số nguyên trong C, Java, và đa số ngôn ngữ có thể vòng qua INT_MIN/INT_MAX trong im lặng: a + 1 vòng về INT_MIN khi a == INT_MAX vì bit nhớ ra bị bỏ.
  • Phép Nhân Nhị Phân
    • Mỗi bit của số nhân được nhân với toàn bộ số bị nhân và các tích bộ phận được cộng lại, dịch theo vị trí bit.
    • Quy tắc một bit: 0×bất kỳ=0, 1×bất kỳ=chính nó (chỉ chép lại).
    • Ví dụ: 101 (5) × 11 (3) = tích bộ phận 101 (×1) và 1010 (×2, dịch trái một vị trí) cộng lại = 1111 (15).
    • Phần cứng làm việc này bằng mạch nhân; nhân hai giá trị 64-bit thường mất vài chu kỳ trên x86 hiện đại (IMUL) nhưng từng rất đắt trên CPU đời đầu — chậm hơn nhiều so với cộng. Đó là lý do nhiều mã cũ dùng thủ thuật shift-and-add.
    • Khi một toán hạng là lũy thừa của 2, phép nhân suy biến thành dịch trái (n × 8 == n << 3). Trình biên dịch làm việc này tự động dưới dạng strength reduction; tương tự cho chia cho lũy thừa của 2 biến thành dịch phải.
  • Phép Chia Nhị Phân
    • Phép chia dài nhị phân tiến hành từng bit: dịch số chia để căn chỉnh với bit cao của số bị chia, trừ nếu vừa (ghi 1 vào thương), dịch sang phải, lặp lại. Phần dư là gì còn lại sau lần trừ cuối.
  • Lưu ý Thực tế
    • Viết giá trị vị trí phía trên các cột (..., 16, 8, 4, 2, 1) là cách nhanh nhất chuyển nhị phân sang thập phân bằng tay. Mỗi '1' đóng góp giá trị vị trí của nó; cộng lại.
    • Dùng hex làm ký pháp trung gian: cứ 4 chữ số nhị phân là 1 chữ số hex, vậy 1010 1011 = 0xAB, không cần tính. Đó là lý do memory dump và mã màu dùng hex.
    • Để ý độ rộng word khi tính trong ngôn ngữ thật: toán tử bitwise của JavaScript âm thầm cắt toán hạng xuống số nguyên có dấu 32-bit, nên mọi tính toán liên quan bit 31+ sẽ hành xử bất ngờ. Máy tính này dùng BigNumber.js để tránh điều đó.

Ngoài thực hành thủ công và máy tính này, mọi ngôn ngữ lập trình thông dụng đều hỗ trợ literal nhị phân (0b1010 trong JS, Python, Rust, C++14+) và bộ toán tử bitwise đầy đủ. Cho công việc nhúng, tham khảo datasheet chip của bạn — bố cục bit thanh ghi gần như luôn được định nghĩa bằng nhị phân.

Bảng chuyển đổi Nhị phân/Thập phân

Nhị phânThập phân
00000
00011
00102
00113
01004
01015
01106
01117
10008
10019
101010
101111
110012
110113
111014
111115

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

Vì nhị phân chỉ có hai ký hiệu, 0 và 1 — ký hiệu "2" đơn giản là không tồn tại trong hệ này. Khi bạn đạt đến số đếm hai trong bất kỳ hệ vị trí nào, bạn phải nhảy sang giá trị vị trí tiếp theo. Trong thập phân, 9 + 1 = 10 vì lý do hệt như vậy: 9 là ký hiệu một chữ số cuối cùng, thêm 1 buộc nhớ sang cột hàng chục. Trong nhị phân, 1 + 1 cho 0 ở cột đơn vị và nhớ 1 sang cột số 2, viết là 10. Giá trị là hai — thứ duy nhất thay đổi là ký pháp. Đó cũng là lý do lập trình viên đùa: "có 10 loại người trên đời: người hiểu nhị phân và người không". Mọi hệ vị trí hoạt động như vậy, gồm hex: F + 1 = 10 (mười sáu), và tam phân: 2 + 1 = 10 (ba).

Bù 2 là cách biểu diễn phổ quát cho số nguyên có dấu trên phần cứng hiện đại, và hoạt động bằng cách diễn giải lại bit quan trọng nhất (MSB) như có trọng số âm. Trong 8 bit, MSB có trọng số −128 thay vì +128. Vậy 11111111 trong bù 2 là −128 + 64 + 32 + 16 + 8 + 4 + 2 + 1 = −1, và 10000000 chính xác là −128. Để đổi dấu một số: lật mọi bit (bù 1) rồi cộng 1. Ví dụ: +5 = 00000101 → lật thành 11111010 → cộng 1 = 11111011 = −5. Bù 2 có ba tính chất hấp dẫn dẫn đến áp dụng phổ quát: (1) chỉ có một số 0 (00000000), không phải hai như sign-magnitude; (2) phép cộng hoạt động giống nhau cho có dấu và không dấu — cùng mạch cộng xử lý cả hai; (3) bit nhớ ra của bit cao bị bỏ, cho số học modular tự nhiên và hành vi tràn dự đoán được. Đánh đổi: dải âm lớn hơn dải dương đúng 1 (8-bit: −128 đến +127), và abs(INT_MIN) tràn.

Toán tử bitwise không phải là tò mò học thuật — chúng xuất hiện liên tục trong code nhạy hiệu năng, code hệ thống và giao thức. AND (&) kiểm tra bit và áp mặt nạ: status & 0x04 kiểm tra bit thứ ba có bật không, là pattern phổ biến để đọc flag trạng thái hoặc trích trường từ cấu trúc đóng gói. OR (|) bật bit: flags = flags | LOG_INFO bật một flag mà không động đến các flag khác. XOR (^) đảo bit hoặc phát hiện khác biệt: x ^ x luôn là 0, dùng trong checksum; XOR-swap đổi hai biến không cần biến tạm; mã hóa XOR với khóa. NOT (~) đảo mọi bit và chủ yếu dùng kết hợp với AND để xóa bit cụ thể: flags = flags & ~LOG_DEBUG tắt LOG_DEBUG không động đến flag khác. Bốn phép này cũng là nền tảng cho trừu tượng cấp cao: bitmap, bloom filter, RoaringBitmap, netmask IP (ip & netmask cho địa chỉ mạng), trộn màu trong đồ họa và hàm hash. Gần như mọi CPU triển khai chúng như chỉ thị một chu kỳ; trong vòng lặp chặt, chúng vượt mọi giải pháp thay thế dùng điều kiện.

Dịch trái (<<) kiểu nào cũng rõ ràng: đệm bằng 0 ở bên phải, bỏ bit ngoài bên trái. Dịch phải có hai phiên bản và bạn phải biết loại nào. Dịch logic (>>> trong JavaScript và Java, lsr trên ARM) luôn đệm 0 ở bên trái, coi số là không dấu — 0xFF >>> 1 = 0x7F. Dịch số học (>> trong JavaScript, Java, C với kiểu có dấu; asr trên ARM) sao chép bit dấu, bảo tồn dấu của số bù 2 — −2 >> 1 = −1, không phải 0x7FFFFFFE. Điều này quan trọng: với trường bit không dấu, dùng logic; với số nguyên có dấu, dùng số học. Xoay (không có toán tử thuần trong C hay JS, intrinsic _rotl/_rotr trên x86) gói bit từ đầu này sang đầu kia thay vì bỏ chúng — hữu ích trong mật mã (AES, ChaCha đều dùng rotation) và trong hàm hash độ rộng cố định cần phân bố bit cân bằng. << bình thường của JavaScript âm thầm cắt kết quả về 32 bit, nên dịch quá bit 31 gây bất ngờ; máy tính này dùng BigNumber.js để giữ chính xác ở mọi độ rộng.

Vì 4 chữ số nhị phân ánh xạ chính xác sang 1 chữ số hex, không thừa. 0000 đến 1111 ứng với 0 đến F. Một giá trị 32-bit là 32 chữ số nhị phân nhưng chỉ 8 chữ số hex, vừa một dòng mã nguồn và dễ đọc thoáng. Hex bảo toàn toàn bộ cấu trúc bit: mỗi ký tự hex cho biết chính xác 4 bit, vậy 0xDEADBEEF nói ngay pattern bit cho lập trình viên, còn 11011110101011011011111011101111 thì hoa mắt. Octal (cơ số 8) cổ hơn và nhóm bit theo 3 — phổ biến trên máy 12-bit và 36-bit thập niên 1960 — nhưng octal không chia chẵn 32, sinh khoảng trống vụng về; hex thắng. Mã màu RGB (#FF8800), địa chỉ MAC, UUID, địa chỉ bộ nhớ, checksum tệp và codepoint Unicode (U+1F600) đều dùng hex vì điều này. Khi debug code mức bit, học cách dịch chữ số hex sang và từ pattern 4-bit nhị phân bằng mắt là một trong những kỹ năng đòn bẩy cao nhất.

Vì số thập phân 0,1 không có biểu diễn nhị phân chính xác. Trong cơ số 2, 0,1 là phân số tuần hoàn không kết thúc 0,000110011001100... — tương tự cách 1/3 là 0,333... trong thập phân. IEEE 754 độ chính xác kép làm tròn đến 52 bit mantissa, cho xấp xỉ lớn hơn 0,1 một chút. Tương tự với 0,2. Khi bạn cộng hai xấp xỉ đã làm tròn và chuyển lại về thập phân, sai số dư trở nên thấy được: 0,30000000000000004. Lũy thừa của 2 — 0,5, 0,25, 0,125 — và bội số nguyên của chúng — 0,75, 0,625 — biểu diễn chính xác. Bất kỳ thứ khác đều có sai số làm tròn. Cho tính toán tài chính hoặc khi cần số học thập phân chính xác, dùng thư viện Decimal (BigDecimal trong Java, Decimal trong Python, BigNumber.js trong JavaScript) hoặc scale về xu nguyên. Cho số học nguyên nhị phân — trọng tâm máy tính này — không có sai số làm tròn: số nguyên đến 2^53 vừa chính xác trong double, và BigNumber.js đưa ta vượt cả giới hạn đó.

Tùy ngôn ngữ và kiểu số nguyên. C/C++ int phụ thuộc cài đặt nhưng thường là 32-bit có dấu: −2.147.483.648 đến +2.147.483.647 (khoảng ±2,1 tỷ). long long là 64-bit có dấu: khoảng ±9,2 tỷ tỷ. Number của JavaScript là double IEEE 754 64-bit, nên biểu diễn chính xác số nguyên đến 2^53 − 1 = 9.007.199.254.740.991 (Number.MAX_SAFE_INTEGER); vượt mức đó, số nguyên kế tiếp không phân biệt được nữa. Toán tử bitwise của JavaScript (&, |, ^, <<, >>, ~) gian lận khác: luôn cắt cả hai toán hạng về số nguyên có dấu 32-bit, rồi chuyển về Number. Nên 0x100000000 | 0 == 0, làm người ta bất ngờ. ES2020 thêm BigInt (literal 123n) cho chính xác tùy ý. Python có int chính xác tùy ý gốc — không bao giờ tràn — nhưng phải trả chi phí runtime. Rust buộc bạn chọn: u32, i64, u128, v.v., và tràn được kiểm tra ở chế độ debug nhưng vòng lại ở release. Máy tính này dùng BigNumber.js để chính xác tùy ý, nên số học nhị phân với hàng trăm chữ số hoạt động như mong đợi.

Việc dùng pattern nhị phân trong toán học có từ trước Leibniz rất lâu — phép nhân Ai Cập cổ đại dùng phân tích nhị phân, và học giả Ấn Độ Pingala mô tả khoảng năm 200 TCN một hệ vị trí nhị phân tương đương để phân tích vần thơ Sanskrit. Nhưng Gottfried Wilhelm Leibniz, trong bài năm 1703 Explication de l'Arithmétique Binaire, đưa ra cách xử lý có hệ thống đầu tiên ở phương Tây về nhị phân như một hệ đếm đầy đủ, với quy tắc cộng, trừ, nhân, chia — và quan sát rõ ràng rằng mọi số có thể biểu diễn chỉ bằng 0 và 1. Leibniz một phần được truyền cảm hứng từ mô tả các quẻ hexagram Kinh Dịch, 64 quẻ của nó ánh xạ chính xác sang số nhị phân 6-bit. Bước nhảy tiếp theo mất 250 năm: đại số logic của George Boole năm 1854 cho thấy mệnh đề có thể được thao tác với hai giá trị chân lý, và luận văn thạc sĩ MIT năm 1937 của Claude Shannon A Symbolic Analysis of Relay and Switching Circuits chứng minh rằng đại số Boolean cài đặt trên rơle điện có thể thực hiện tính toán tùy ý — nền tảng của mọi máy tính số. Z3 của Konrad Zuse (1941) và ENIAC (1945) theo sau không lâu. Mọi CPU ngày nay rốt cuộc là một cỗ máy mạch Boolean khổng lồ theo bản thiết kế của Shannon.
Máy tính nhị phân — Máy tính nhị phân: cộng, trừ, nhân, chia, AND, OR, XOR, NOT, shift. Hỗ trợ BigNumber, đổi cơ số, giải thích bù 2.
Máy tính nhị phân