Bài 31.528 phút đọc
Thủ thuật bit và hàm dựng sẵn
Sau bài này bạn sẽ làm được
- Kiểm tra lũy thừa của hai, xóa bit thấp nhất, lấy bit thấp nhất
- Đếm bit một bằng ba cách và đo tốc độ
- Đảo thứ tự byte và kiểm tra thứ tự byte của máy
- Dùng các hàm dựng sẵn của GCC và biết điều kiện của chúng
Mười biểu thức mà mỗi cái thay được một vòng lặp. Chúng xuất hiện trong thư viện chuẩn, trong nhân hệ điều hành, và trong mọi bài phỏng vấn về thao tác bit. Bài này giải thích vì sao chúng chạy, chứ không chỉ liệt kê.
#Bit một thấp nhất
x & (x - 1) /* XOA bit 1 thap nhat */
x & (-x) /* LAY rieng bit 1 thap nhat */
x | (x + 1) /* BAT bit 0 thap nhat */terminal
./thu-thuat
y = 0xB4 = 180 y & (y-1) = 0xB0 (xoa bit 1 thap nhat) y & (-y) = 0x04 (lay bit 1 thap nhat)
Kiểm tra lũy thừa của hai
int la_luy_thua_2(uint32_t x) {
return x && !(x & (x - 1));
}
/* Luy thua cua 2 co DUNG MOT bit bang 1.
Xoa bit do di thi con 0.
Dieu kien "x &&" o dau la bat buoc: voi x = 0 thi
0 & (0-1) = 0 & 0xFFFFFFFF = 0, nen ve phai la dung,
nhung 0 KHONG phai luy thua cua 2. */terminal
./thu-thuat --luy-thua
0 -> khong 1 -> co 2 -> co 3 -> khong 4 -> co 5 -> khong 6 -> khong 7 -> khong 8 -> co 9 -> khong 1024 -> co
Làm tròn lên lũy thừa của hai gần nhất
uint32_t lam_tron_len(uint32_t x) {
if (x == 0) return 1;
--x; /* de x da la luy thua 2 thi giu nguyen */
x |= x >> 1; /* bat bit ben phai bit cao nhat */
x |= x >> 2;
x |= x >> 4;
x |= x >> 8;
x |= x >> 16; /* gio moi bit tu bit cao nhat tro xuong deu la 1 */
return x + 1; /* cong 1 lan qua het, cho luy thua ke tiep */
}
/* Nam dong dich: 1 + 2 + 4 + 8 + 16 = 31, du de lan het 32 bit.
Voi uint64_t thi them mot dong "x |= x >> 32". */terminal
./thu-thuat --lam-tron
0 -> 1 1 -> 1 2 -> 2 3 -> 4 4 -> 4 5 -> 8 6 -> 8 7 -> 8 8 -> 8 9 -> 16 1000 -> 1024
#Đếm bit một, bốn cách
/* Cach 1: duyet 32 bit, luon 32 vong */
int dem_duyet(uint32_t x) {
int c = 0;
for (int i = 0; i < 32; ++i) c += (x >> i) & 1u;
return c;
}
/* Cach 2: Brian Kernighan, so vong bang so bit 1 */
int dem_kern(uint32_t x) {
int c = 0;
while (x) { x &= x - 1; ++c; }
return c;
}
/* Cach 3: tra bang 256 muc, bon lan tra */
static unsigned char BANG[256];
void dung_bang(void) {
for (int i = 0; i < 256; ++i) BANG[i] = (unsigned char)dem_kern((uint32_t)i);
}
int dem_bang(uint32_t x) {
return BANG[x & 0xFF] + BANG[(x >> 8) & 0xFF]
+ BANG[(x >> 16) & 0xFF] + BANG[x >> 24];
}
/* Cach 4: ham dung san, dung lenh CPU neu co */
int dem_builtin(uint32_t x) { return __builtin_popcount(x); }terminal
./dem-bit
gia tri duyet kern bang popcount 0x00000000 0 0 0 0 0x00000001 1 1 1 1 0x00000007 3 3 3 3 0x000000FF 8 8 8 8 0xF0F0F0F0 16 16 16 16 0xFFFFFFFF 32 32 32 32 0x00003039 6 6 6 6
terminal
# Ba mươi triệu lần đếm, gcc -O2
gcc -std=c11 -O2 -o bench bench.c && ./bench
duyet tung bit : 0.519 giay Kernighan : 0.147 giay tra bang 256 : 0.041 giay popcount : 0.102 giay
# Cùng chương trình, thêm cờ nói với gcc rằng CPU có lệnh POPCNT
gcc -std=c11 -O2 -mpopcnt -o bench bench.c && ./bench
tra bang 256 : 0.040 giay popcount : 0.038 giay
| Cách | Thời gian đo được | Số vòng lặp | Dùng khi |
|---|---|---|---|
| Duyệt 32 bit | 0,519 s | Luôn 32 | Không bao giờ, trừ khi dạy học |
| Kernighan | 0,147 s | Bằng số bit 1 | Dữ liệu thưa, ít bit 1 |
| Tra bảng 256 | 0,041 s | Không có, bốn lần tra | Không có lệnh CPU, và có 256 byte để trống |
| popcount, không cờ | 0,102 s | Không có | Mặc định, khả chuyển |
| popcount, có -mpopcnt | 0,038 s | Một lệnh | Biết chắc CPU đích |
#Hàm dựng sẵn của GCC
| Hàm | Trả về | Điều kiện | Lệnh CPU tương ứng |
|---|---|---|---|
| __builtin_popcount(x) | Số bit bằng 1 | Không có | POPCNT |
| __builtin_clz(x) | Số bit 0 ở đầu | x PHẢI khác 0 | LZCNT hoặc BSR |
| __builtin_ctz(x) | Số bit 0 ở cuối | x PHẢI khác 0 | TZCNT hoặc BSF |
| __builtin_ffs(x) | Vị trí bit 1 thấp nhất, đếm từ 1 | Không có | |
| __builtin_parity(x) | Số bit 1 chẵn hay lẻ | Không có | |
| __builtin_bswap16/32/64(x) | Đảo thứ tự byte | Không có | BSWAP |
terminal
./builtin
clz(1) = 31, ctz(1) = 0 clz(0x100) = 23, ctz(0x100) = 8 clz(0x8000) = 16, ctz(0x8000) = 15
#Thứ tự byte và đảo bit
/* Kiem tra thu tu byte cua may */
static int nho_truoc(void) {
uint32_t u = 1;
return *(const unsigned char *)&u == 1;
}
/* Dao thu tu byte cua mot so 32 bit */
uint32_t dao_byte(uint32_t x) {
return ((x & 0x000000FFu) << 24)
| ((x & 0x0000FF00u) << 8)
| ((x & 0x00FF0000u) >> 8)
| ((x & 0xFF000000u) >> 24);
}terminal
./dao
dao_byte(0x12345678) = 0x78563412 bswap32 (0x12345678) = 0x78563412
Đảo thứ tự bit, không phải byte
uint32_t dao_bit(uint32_t x) {
/* Doi cho tung cap bit */
x = ((x >> 1) & 0x55555555u) | ((x & 0x55555555u) << 1);
/* Doi cho tung cap hai bit */
x = ((x >> 2) & 0x33333333u) | ((x & 0x33333333u) << 2);
/* Doi cho tung cap bon bit */
x = ((x >> 4) & 0x0F0F0F0Fu) | ((x & 0x0F0F0F0Fu) << 4);
/* Roi doi cho cac byte */
return __builtin_bswap32(x);
}terminal
./dao --bit
dao_bit (0x80000001) = 0x80000001 dao_bit (0x12345678) = 0x1E6A2C48
#Bảng mười thủ thuật
| Việc | Biểu thức | Ghi chú |
|---|---|---|
| Kiểm lẻ | x & 1 | Nhanh hơn x % 2 với kiểu có dấu |
| Nhân 2 mũ n | x << n | Chỉ khi không tràn |
| Chia 2 mũ n | x >> n | Chỉ với x không âm, hoặc kiểu không dấu |
| Lấy dư 2 mũ n | x & ((1u << n) - 1) | Chỉ với kiểu không dấu |
| Xóa bit 1 thấp nhất | x & (x - 1) | |
| Lấy bit 1 thấp nhất | x & (0u - x) | Dùng 0u - x, không dùng -x |
| Kiểm lũy thừa 2 | x && !(x & (x - 1)) | Điều kiện x khác 0 bắt buộc |
| Đảo dấu | (~x) + 1u | Chính là bù hai |
| Giá trị tuyệt đối không rẽ nhánh | (x + m) ^ m với m = x >> 31 | Giả định dịch số học |
| Đổi hai biến không dùng biến tạm | a ^= b; b ^= a; a ^= b; | HỎNG nếu a và b cùng địa chỉ |
Tự làm thử
- Chạy
x & (x-1)vàx & (-x)trên0xB4rồi vẽ bit ra giấy để kiểm. - Cài bốn cách đếm bit và xác nhận chúng cho cùng kết quả với bảy giá trị trong bài.
- Đo tốc độ bốn cách trên máy bạn, rồi thêm
-mpopcntvà đo lại. - Gọi
__builtin_clz(0)và xem kết quả trên máy bạn. - Cài
lam_tron_lenvà kiểm với 0 tới 9 cùng 1000. - Cài
dao_bitvà kiểm bằng phép đảo hai lần trên một triệu giá trị. - Gọi
hoan_doi_xor(&x, &x)và xác nhậnxthành 0.
Trình chấm điểm tự động sẽ được bổ sung ở giai đoạn sau. Hiện tại bạn tự chạy thử trên máy.
Tóm tắt
x & (x - 1)xóa bit một thấp nhất, vàx & (0u - x)lấy riêng nó.- Bốn cách đếm bit chênh nhau tới mười ba lần, và
__builtin_popcountchỉ nhanh nhất khi bạn thêm-mpopcnt. __builtin_clzvà__builtin_ctzvới đối số 0 là hành vi không xác định. C23 cóstdbit.hkhông có cái bẫy đó.- Đọc dữ liệu từ tệp hay mạng thì ghép byte tường minh, đừng đọc rồi đảo theo thứ tự byte của máy.
- Thủ thuật XOR để hoán đổi hỏng khi hai con trỏ cùng địa chỉ, và nó còn chậm hơn bản dùng biến tạm.