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

Căn chỉnh

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

  • Tra yêu cầu căn chỉnh của mọi kiểu cơ bản
  • Cấp phát bộ nhớ căn chỉnh theo dòng bộ đệm
  • Giải thích vì sao truy cập lệch căn chỉnh là UB
  • Dùng memcpy thay cho ép con trỏ lệch

Trên x86, truy cập lệch căn chỉnh chỉ chậm hơn một chút, nên gần như không ai để ý. Trên ARM cũ thì nó làm chương trình sập. Và trong cả hai trường hợp, chuẩn C gọi nó là hành vi không xác định.

#Căn chỉnh là gì

Yêu cầu căn chỉnh (alignment requirement)
Với mỗi kiểu T, một số nguyên là lũy thừa của hai. Địa chỉ của mọi đối tượng kiểu T phải chia hết cho số đó. Lấy bằng _Alignof(T).
#include <stdalign.h>       /* cho alignof va alignas, ten ngan */

_Alignof(char)     /* 1: dia chi nao cung duoc */
_Alignof(int)      /* 4: dia chi phai chia het cho 4 */
_Alignof(double)   /* 8 */

alignof(int)       /* giong _Alignof, ten ngan qua stdalign.h */

/* Va tu C23 thi alignof la tu khoa, khong can header. */

#Đo trên máy của bạn

bocuc.c
#include <stdio.h>
#include <stdalign.h>

int main(void) {
    printf("char %d  short %d  int %d  long %d  long long %d\n",
           (int)_Alignof(char), (int)_Alignof(short), (int)_Alignof(int),
           (int)_Alignof(long), (int)_Alignof(long long));
    printf("float %d  double %d  void* %d  max_align_t %d\n",
           (int)_Alignof(float), (int)_Alignof(double),
           (int)_Alignof(void *), (int)_Alignof(max_align_t));
    return 0;
}
terminal
# MinGW-W64, x86-64, Windows
gcc -std=c11 -O2 -o bocuc.exe bocuc.c && ./bocuc.exe
char 1  short 2  int 4  long 4  long long 8
float 4  double 8  void* 8  max_align_t 16
Kiểux86-64x86 32 bitARM 32 bit
char111
short222
int444
long4 hoặc 844
long long84 hoặc 88
double84 hoặc 88
void *844
max_align_t168 hoặc 168

#Bốn lý do nó quan trọng

Một: tốc độ trên x86

/* Truy cap lech can chinh tren x86-64 hien dai:
     - voi mot lan doc: cham hon khoang 1 den 2 phan tram
     - khi vat qua ranh gioi dong bo dem 64 byte: cham hon dang ke
     - voi lenh SIMD: co the cham gap doi

   Nen tren x86 thi day la van de HIEU NANG, khong phai van de dung sai.
   Va do la ly do nhieu nguoi khong bao gio gap no. */

Hai: sập trên ARM và SPARC

char buf[10];
int *p = (int *)(buf + 1);       /* dia chi khong chia het cho 4 */
int x = *p;

/* Tren x86:        chay, cham hon mot chut
   Tren ARM32 cu:   SIGBUS, chuong trinh chet
   Tren ARM64:      thuong chay, nhung lenh nap doi va SIMD thi khong
   Tren SPARC:      SIGBUS

   Va tren MOI may, chuan goi day la hanh vi khong xac dinh. */

Ba: SIMD bắt buộc

#include <immintrin.h>

__m128 v = _mm_load_ps(p);       /* BAT BUOC p can chinh 16 byte,
                                    khong thi sap */
__m128 v = _mm_loadu_ps(p);      /* ban "u" cho phep lech, cham hon */

__m256d w = _mm256_load_pd(p);   /* AVX: bat buoc 32 byte */

/* Nen khi viet ma SIMD, can chinh khong phai tuy chon. */

Bốn: dòng bộ đệm và chia sẻ giả

Đây là lý do tinh vi nhất và ảnh hưởng lớn nhất tới hiệu năng đa luồng. Xem mục cuối bài.

#_Alignas và cấp phát căn chỉnh

c11.c
#include <stdio.h>
#include <stdlib.h>
#include <stdalign.h>

int main(void) {
    alignas(64) char bo_dem[128];
    printf("bo_dem can chinh 64: %s\n",
           ((size_t)bo_dem % 64 == 0) ? "dung" : "sai");

    void *a = _aligned_malloc(1024, 64);   /* MinGW: khong co aligned_alloc */
    printf("cap phat can chinh 64: %s\n",
           (a && (size_t)a % 64 == 0) ? "dung" : "khong ho tro");
    _aligned_free(a);

    return 0;
}
terminal
gcc -std=c11 -O2 -Wall -Wextra -o c11.exe c11.c && ./c11.exe
bo_dem can chinh 64: dung
cap phat can chinh 64: dung
Cách cấp phát căn chỉnhChuẩnCó ở đâu
aligned_alloc(can, n)C11Linux, macOS. KHÔNG có trên MinGW
posix_memalign(&p, can, n)POSIXLinux, macOS
_aligned_malloc(n, can)MicrosoftWindows, MSVC và MinGW
alignas(64) char b[n]C11Mọi nơi, nhưng chỉ cho biến, không cho cấp phát động
Tự căn chỉnh bằng tayC89Mọi nơi
Một lớp bao chạy được ở mọi nơi
#include <stdlib.h>

static void *cap_can_chinh(size_t can, size_t n) {
#if defined(_WIN32)
    return _aligned_malloc(n, can);
#elif defined(_ISOC11_SOURCE) || __STDC_VERSION__ >= 201112L
    /* lam tron n len boi cua can, cho an toan truoc C23 */
    size_t tron = (n + can - 1) / can * can;
    return aligned_alloc(can, tron);
#else
    void *p = NULL;
    if (posix_memalign(&p, can, n) != 0) return NULL;
    return p;
#endif
}

static void giai_phong_can_chinh(void *p) {
#if defined(_WIN32)
    _aligned_free(p);
#else
    free(p);
#endif
}

#Truy cập lệch căn chỉnh

Ép con trỏ vào giữa bộ đệm
unsigned char bo_dem[1024];
fread(bo_dem, 1, sizeof bo_dem, f);

/* Doc mot so 32 bit o vi tri 5 */
uint32_t n = *(uint32_t *)(bo_dem + 5);

/* HAI van de:
     1. can chinh: bo_dem + 5 co the khong chia het cho 4
     2. trung bi danh: doc mang char qua uint32_t *, Bai 33.6

   Tren x86 thi chay. Tren ARM32 thi sap. */
memcpy hoặc đọc từng byte
unsigned char bo_dem[1024];
fread(bo_dem, 1, sizeof bo_dem, f);

uint32_t n;
memcpy(&n, bo_dem + 5, sizeof n);

/* Giai quyet CA HAI van de:
     - memcpy lam viec tren byte, khong co yeu cau can chinh
     - memcpy hop le ve trung bi danh

   Va Bai 33.6 da do: GCC thay memcpy voi kich thuoc co dinh
   bang dung mot lenh nap. Khong ton them gi. */

/* Hoac doc tung byte, neu can doc lap thu tu byte: */
uint32_t n = (uint32_t)bo_dem[5] << 24 | (uint32_t)bo_dem[6] << 16
           | (uint32_t)bo_dem[7] <<  8 | (uint32_t)bo_dem[8];
# Ba cach phat hien truy cap lech

gcc -fsanitize=alignment prog.c     # UBSan, bat luc chay
gcc -Wcast-align prog.c             # canh bao luc dich khi ep con tro
                                    # sang kieu co can chinh chat hon
gcc -Wcast-align=strict prog.c      # nghiem hon, ke ca tren x86

# -Wcast-align mac dinh chi canh bao tren nen tang KHONG cho phep
# truy cap lech. Tren x86 thi no im lang, nen phai dung ban "strict"
# neu ban muon ma chay duoc ca tren ARM.
terminal
gcc -std=c11 -Wall -Wcast-align=strict -c lech.c
lech.c: In function 'doc':
lech.c:6:19: warning: cast increases required alignment of target type [-Wcast-align]
     uint32_t *p = (uint32_t *)(bo_dem + 5);
                   ^

#Chia sẻ giả

Chia sẻ giả (false sharing)
Hai luồng ghi vào hai biến khác nhau nhưng nằm chung một dòng bộ đệm. Phần cứng phải đồng bộ cả dòng giữa hai lõi, nên hai biến không liên quan gì tới nhau lại làm chậm nhau.
Bốn biến trong một dòng bộ đệm
typedef struct {
    long dem;
} BoDem;

BoDem ds[4];        /* bon bo dem, moi cai 8 byte -> ca bon
                       nam trong MOT dong bo dem 64 byte */

/* Bon luong, moi luong tang ds[i].dem mot trieu lan.

   Moi lan mot luong ghi, dong bo dem bi danh dau la khong hop le
   o ba loi kia. Chung phai nap lai. Ket qua: cham hon nhieu lan
   so voi mot luong chay bon trieu lan. */
Mỗi biến một dòng riêng
typedef struct {
    alignas(64) long dem;
} BoDem;

BoDem ds[4];        /* moi phan tu 64 byte -> moi cai mot dong rieng */

/* Bay gio bon luong khong dung chung dong nao ca,
   va chung chay doc lap voi toc do gan nhu bang bon lan mot luong.

   Chi phi: 256 byte thay vi 32 byte. Voi bon bo dem thi khong dang ke. */

/* C23 co hai hang so cho viec nay: */
#include <stddef.h>
alignas(__STDC_HOSTED__) ...

/* Truoc C23 thi tu dinh nghia: */
#define DONG_BO_DEM 64

Tự làm thử

  1. In _Alignof của mọi kiểu cơ bản trên máy bạn.
  2. Chứng minh malloc luôn trả về địa chỉ chia hết cho _Alignof(max_align_t).
  3. Cấp phát căn chỉnh 64 byte và kiểm tra bằng phép chia dư.
  4. Ép một con trỏ lệch và chạy với -fsanitize=alignment.
  5. Bật -Wcast-align=strict trên một dự án và đếm cảnh báo.
  6. Viết thí nghiệm chia sẻ giả với bốn luồng, đo trước và sau khi thêm alignas(64).

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

  • _Alignof(T) là số mà địa chỉ của mọi đối tượng kiểu T phải chia hết cho.
  • malloc bảo đảm căn chỉnh đủ cho mọi kiểu cơ bản, nhưng không bảo đảm căn chỉnh lớn hơn thế.
  • aligned_alloc không có trên MinGW; Windows dùng _aligned_malloc với thứ tự đối số ngược lại và phải giải phóng bằng _aligned_free.
  • memcpy giải quyết cả vấn đề căn chỉnh lẫn vấn đề trùng bí danh, và không tốn thêm lệnh nào.
  • Chia sẻ giả làm chương trình đa luồng chậm hơn bản một luồng, và alignas(64) là cách chữa.