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ông | float 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ông | long 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ông | Hai 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ụng | Khi nào dùng |
|---|---|---|
| -fstrict-aliasing | Bật quy tắc, mặc định từ -O2 | Mặc định, để yên |
| -fno-strict-aliasing | Tắt quy tắc, trình biên dịch giả định mọi con trỏ đều có thể trùng | Mã cũ không sửa được ngay |
| -Wstrict-aliasing=1 | Cảnh báo nhiều, nhiều báo giả | Ít khi |
| -Wstrict-aliasing=2 | Cân bằng, nằm trong -Wall từ -O2 | Nên bật |
| -Wstrict-aliasing=3 | Chỉ 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ử
- Chạy
alias2.cở-O0,-O2và-O2 -fno-strict-aliasing, so ba kết quả. - Đọc hợp ngữ của
lam_roiở hai cấu hình và chỉ ra lệnh đọc bị bỏ. - Viết ba hàm đọc bit của
floatvà xác nhận hợp ngữ giống hệt nhau. - Viết lại
Q_rsqrtbằngmemcpyvà so hợp ngữ với bản gốc. - Kiểm tra xem
-Wstrict-aliasing=2có bắt được vi phạm tronglam_roikhông. - 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ệ
charvàunsigned char. - Vi phạm cho ra hai kết quả khác nhau ở
-O0và-O2: hàm trả về 1 trong khi ô nhớ chứa 1073741824. - Ba cách đọc bit của
floatsinh ra đúng cùng một lệnhmovd 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
memcpylà lựa chọn an toàn ở mọi nơi. -Wstrict-aliasingchỉ bắt được trường hợp dễ thấy nhất, nên không dựa vào nó được.