Bỏ qua điều hướng, tới nội dung chính
Học C
Bài 33.630 phút đọc

Strict aliasing

Sau bài này bạn sẽ làm được

  • Phát biểu quy tắc strict aliasing và các ngoại lệ của nó
  • Chỉ ra kết quả sai do vi phạm khi bật -O2
  • Dùng memcpy và union làm cách đúng, kèm hợp ngữ chứng minh không tốn thêm lệnh
  • Biết khi nào -fno-strict-aliasing là lựa chọn hợp lý

Đọc bit của một float bằng cách ép con trỏ sang int là đoạn mã ai cũng từng viết, và nó là hành vi không xác định. Điều thú vị là cách viết đúng không tốn thêm một lệnh CPU nào.

#Quy tắc và các ngoại lệ

Strict aliasing
Một đối tượng chỉ được truy cập qua con trỏ có kiểu tương thích với kiểu thật của nó. Nếu bạn ghi một float rồi đọc nó qua int *, trình biên dịch được phép coi như hai truy cập đó không liên quan gì tới nhau.
/* Kieu nao duoc phep truy cap mot doi tuong kieu T: */

T                          /* dung kieu do */
const T, volatile T        /* T co them tu khoa */
signed T, unsigned T       /* ban co dau va khong dau tuong ung */
struct hoac union chua T   /* ke ca long nhieu tang */
char, signed char          /* NGOAI LE: luon hop le */
unsigned char              /* NGOAI LE: luon hop le */

/* MOI kieu khac deu vi pham. */
Đoạn mãHợp lệVì sao
int i; float *p = (float *)&i; *p = 1.0f;Khôngfloat không tương thích với int
int i; unsigned *p = (unsigned *)&i;CóBản không dấu tương ứng
int i; long *p = (long *)&i;Khônglong là kiểu khác, kể cả khi cùng độ rộng
int i; char *p = (char *)&i;Cóchar là ngoại lệ
int i; unsigned char *p = ...CóNgoại lệ, đây là cách đọc byte đúng
struct A a; struct B *p = ...KhôngHai struct khác nhau
int i; const int *p = &i;CóChỉ thêm const
float f; memcpy(&i, &f, 4);Cómemcpy làm việc trên byte

#Nó hỏng thật, không phải lý thuyết

alias2.c
#include <stdio.h>

int lam_roi(int *pi, float *pf) {
    *pi = 1;
    *pf = 2.0f;

    return *pi;
}

int main(void) {
    int o = 0;

    int kq = lam_roi(&o, (float *)&o);     /* hai con tro, mot o nho */

    printf("ham tra ve : %d\n", kq);
    printf("gia tri that: %d\n", o);

    return 0;
}
terminal
gcc -std=c11 -O0 -o a2 alias2.c && ./a2
ham tra ve : 1073741824
gia tri that: 1073741824
gcc -std=c11 -O2 -o a2 alias2.c && ./a2
ham tra ve : 1
gia tri that: 1073741824
gcc -std=c11 -O2 -fno-strict-aliasing -o a2 alias2.c && ./a2
ham tra ve : 1073741824
gia tri that: 1073741824
terminal
# GCC có cảnh báo, nhưng phải bật riêng
gcc -std=c11 -O2 -Wall -Wextra -Wstrict-aliasing=2 -c alias.c
alias.c: In function 'doc_bit_sai':
alias.c:5:25: warning: dereferencing type-punned pointer will break strict-aliasing rules [-Wstrict-aliasing]
     return *(unsigned *)&f;
                         ^~

#Ba cách đúng, và cả ba đều miễn phí

alias.c
__attribute__((noinline)) unsigned doc_bit_sai(float f) {
    return *(unsigned *)&f;                  /* VI PHAM */
}

__attribute__((noinline)) unsigned doc_bit_memcpy(float f) {
    unsigned u;
    memcpy(&u, &f, sizeof u);                /* DUNG */
    return u;
}

__attribute__((noinline)) unsigned doc_bit_union(float f) {
    union { float f; unsigned u; } t = { .f = f };
    return t.u;                              /* DUNG trong C */
}
terminal
gcc -std=c11 -O2 -o alias alias.c && ./alias
sai    : 3F800000
memcpy : 3F800000
union  : 3F800000
# Và đây là điểm quan trọng nhất của cả bài
gcc -std=c11 -O2 -S -masm=intel -o - alias.c
doc_bit_sai:
    movd eax, xmm0
    ret

doc_bit_memcpy:
    movd eax, xmm0
    ret

doc_bit_union:
    movd eax, xmm0
    ret
Ép con trỏ
float f = 1.0f;
unsigned u = *(unsigned *)&f;

/* Va bien the pho bien khong kem: */

typedef struct { uint8_t byte[4]; } Goi;

Goi g;
uint32_t n = *(uint32_t *)g.byte;    /* vi pham, va co the lech can chinh */
memcpy hoặc đọc từng byte
#include <string.h>

float f = 1.0f;
unsigned u;
memcpy(&u, &f, sizeof u);

/* Doc so tu goi tin, dung ca ve trung bi danh lan can chinh: */

uint32_t n;
memcpy(&n, g.byte, sizeof n);

/* Hoac doc tung byte, khong phu thuoc thu tu byte cua may: */

uint32_t n = (uint32_t)g.byte[0] << 24
           | (uint32_t)g.byte[1] << 16
           | (uint32_t)g.byte[2] <<  8
           | (uint32_t)g.byte[3];

#Hai cờ dịch

CờTác dụngKhi nào dùng
-fstrict-aliasingBật quy tắc, mặc định từ -O2Mặc định, để yên
-fno-strict-aliasingTắt quy tắc, trình biên dịch giả định mọi con trỏ đều có thể trùngMã cũ không sửa được ngay
-Wstrict-aliasing=1Cảnh báo nhiều, nhiều báo giảÍt khi
-Wstrict-aliasing=2Cân bằng, nằm trong -Wall từ -O2Nên bật
-Wstrict-aliasing=3Chỉ trường hợp chắc chắn, ít báo giảMặc định của -Wall

#Bốn chỗ bạn sẽ gặp nó

Một: đọc và ghi định dạng nhị phân

/* Doc mot goi tin hoac mot ban ghi tu tep: */

unsigned char bo_dem[1024];
fread(bo_dem, 1, sizeof bo_dem, f);

TieuDe *td = (TieuDe *)bo_dem;      /* VI PHAM, va co the lech can chinh */

/* Cach dung: */

TieuDe td;
memcpy(&td, bo_dem, sizeof td);     /* dung ve ca hai mat */

/* Bai 18.5 va Bai 18.8 da noi ve dinh dang tep nhi phan. */

Hai: thư viện toán viết tay

/* Thuat toan can bac hai nguoc noi tieng cua Quake III: */

float Q_rsqrt(float number) {
    long i;
    float x2, y;

    x2 = number * 0.5F;
    y  = number;
    i  = *(long *)&y;                       /* VI PHAM */
    i  = 0x5f3759df - (i >> 1);
    y  = *(float *)&i;                      /* VI PHAM */
    y  = y * (1.5F - (x2 * y * y));

    return y;
}

/* Doan ma nay noi tieng, va no la UB. Ban 1999 chay dung vi
   trinh bien dich luc do khong khai thac quy tac.

   Ban dung: doi hai dong ep kieu thanh memcpy. Hop ngu giong het,
   va ma tro nen hop le. */

Ba: bộ cấp phát và bể nhớ tự viết

/* Mot be nho don gian: */

static unsigned char be[4096];
static size_t da_dung = 0;

void *cap(size_t n) {
    void *p = be + da_dung;
    da_dung += n;
    return p;
}

/* Nguoi dung viet: */
SinhVien *sv = cap(sizeof *sv);
sv->ma = 1;                          /* ghi kieu SinhVien vao mang char */

/* Truong hop nay THUC TE la hop le, vi chuan noi vung nho tra ve
   tu ham cap phat khong co "kieu khai bao": kieu cua no duoc quyet dinh
   boi lan ghi dau tien.

   Nhung mang tinh "be" thi CO kieu khai bao la unsigned char[4096],
   nen viec nay thuoc vung xam. Cach chac chan:

     - dung malloc that
     - hoac khai bao be bang mot union chua moi kieu ban dinh dat vao
     - hoac dung _Alignas(max_align_t) va chap nhan phu thuoc trinh bien dich */

Bốn: hàm băm và so sánh nhanh

/* So sanh 16 byte bang bon phep so sanh 32 bit: */

int bang_nhau(const char a[16], const char b[16]) {
    const uint32_t *pa = (const uint32_t *)a;    /* VI PHAM va co the lech */
    const uint32_t *pb = (const uint32_t *)b;

    return pa[0] == pb[0] && pa[1] == pb[1]
        && pa[2] == pb[2] && pa[3] == pb[3];
}

/* Cach dung, va nhanh khong kem: */

int bang_nhau(const char a[16], const char b[16]) {
    return memcmp(a, b, 16) == 0;
}

/* memcmp cua glibc voi kich thuoc co dinh duoc noi tuyen thanh
   hai lenh SIMD. Nhanh hon ban viet tay o tren. */

Tự làm thử

  1. Chạy alias2.c ở -O0, -O2 và -O2 -fno-strict-aliasing, so ba kết quả.
  2. Đọc hợp ngữ của lam_roi ở hai cấu hình và chỉ ra lệnh đọc bị bỏ.
  3. Viết ba hàm đọc bit của float và xác nhận hợp ngữ giống hệt nhau.
  4. Viết lại Q_rsqrt bằng memcpy và so hợp ngữ với bản gốc.
  5. Kiểm tra xem -Wstrict-aliasing=2 có bắt được vi phạm trong lam_roi không.
  6. Tìm trong một dự án cũ mọi chỗ ép con trỏ giữa hai kiểu không tương thích.

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

  • Chỉ được truy cập một đối tượng qua kiểu tương thích, cộng với ngoại lệ char và unsigned char.
  • Vi phạm cho ra hai kết quả khác nhau ở -O0 và -O2: hàm trả về 1 trong khi ô nhớ chứa 1073741824.
  • Ba cách đọc bit của float sinh ra đúng cùng một lệnh movd eax, xmm0, nên bản đúng hoàn toàn miễn phí.
  • Union hợp lệ trong C nhưng không trong C++, nên memcpy là lựa chọn an toàn ở mọi nơi.
  • -Wstrict-aliasing chỉ bắt được trường hợp dễ thấy nhất, nên không dựa vào nó được.