Bỏ qua điều hướng, tới nội dung chính
Học C
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áchThời gian đo đượcSố vòng lặpDùng khi
Duyệt 32 bit0,519 sLuôn 32Không bao giờ, trừ khi dạy học
Kernighan0,147 sBằng số bit 1Dữ liệu thưa, ít bit 1
Tra bảng 2560,041 sKhông có, bốn lần traKhông có lệnh CPU, và có 256 byte để trống
popcount, không cờ0,102 sKhông cóMặc định, khả chuyển
popcount, có -mpopcnt0,038 sMột lệnhBiết chắc CPU đích
Ba mươi triệu lần đếm trên cùng một máy. Con số tùy CPU, hãy tự đo.

#Hàm dựng sẵn của GCC

HàmTrả vềĐiều kiệnLệnh CPU tương ứng
__builtin_popcount(x)Số bit bằng 1Không cóPOPCNT
__builtin_clz(x)Số bit 0 ở đầux PHẢI khác 0LZCNT hoặc BSR
__builtin_ctz(x)Số bit 0 ở cuốix PHẢI khác 0TZCNT hoặc BSF
__builtin_ffs(x)Vị trí bit 1 thấp nhất, đếm từ 1Không có
__builtin_parity(x)Số bit 1 chẵn hay lẻKhông có
__builtin_bswap16/32/64(x)Đảo thứ tự byteKhông cóBSWAP
Thêm hậu tố l cho unsigned long và ll cho unsigned long long, ví dụ __builtin_popcountll.
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ệcBiểu thứcGhi chú
Kiểm lẻx & 1Nhanh hơn x % 2 với kiểu có dấu
Nhân 2 mũ nx << nChỉ khi không tràn
Chia 2 mũ nx >> nChỉ với x không âm, hoặc kiểu không dấu
Lấy dư 2 mũ nx & ((1u << n) - 1)Chỉ với kiểu không dấu
Xóa bit 1 thấp nhấtx & (x - 1)
Lấy bit 1 thấp nhấtx & (0u - x)Dùng 0u - x, không dùng -x
Kiểm lũy thừa 2x && !(x & (x - 1))Điều kiện x khác 0 bắt buộc
Đảo dấu(~x) + 1uChính là bù hai
Giá trị tuyệt đối không rẽ nhánh(x + m) ^ m với m = x >> 31Giả định dịch số học
Đổi hai biến không dùng biến tạma ^= b; b ^= a; a ^= b;HỎNG nếu a và b cùng địa chỉ

Tự làm thử

  1. Chạy x & (x-1) và x & (-x) trên 0xB4 rồi vẽ bit ra giấy để kiểm.
  2. 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.
  3. Đo tốc độ bốn cách trên máy bạn, rồi thêm -mpopcnt và đo lại.
  4. Gọi __builtin_clz(0) và xem kết quả trên máy bạn.
  5. Cài lam_tron_len và kiểm với 0 tới 9 cùng 1000.
  6. Cài dao_bit và kiểm bằng phép đảo hai lần trên một triệu giá trị.
  7. Gọi hoan_doi_xor(&x, &x) và xác nhận x thà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_popcount chỉ nhanh nhất khi bạn thêm -mpopcnt.
  • __builtin_clz và __builtin_ctz với đối số 0 là hành vi không xác định. C23 có stdbit.h khô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.