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

Sắp xếp trường để tiết kiệm bộ nhớ

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

  • Sắp xếp trường theo kích thước giảm dần
  • Đo mức tiết kiệm trên một mảng lớn
  • Dùng pahole để tìm chỗ lãng phí
  • Biết khi nào thứ tự khai báo quan trọng hơn kích thước

Đổi thứ tự ba dòng khai báo, tiết kiệm ba mươi ba phần trăm bộ nhớ, không đổi một dòng logic nào. Đây là thay đổi có tỉ lệ lợi ích trên công sức cao nhất trong toàn bộ phần này.

#Hai mươi bốn xuống mười sáu

Cùng ba trường, hai thứ tự
struct Kem { char a; double b; char c; };
struct Tot { double b; char a; char c; };
terminal
gcc -std=c11 -O2 -o bocuc.exe bocuc.c && ./bocuc.exe
sizeof(Kem) = 24
sizeof(Tot) = 16
Bản đồ byte của cả hai
struct Kem { char a; double b; char c; };

byte:  0    1 2 3 4 5 6 7    8 .. 15      16   17 .. 23
     +----+--------------+-------------+----+-----------+
     | a  |   DEM 7      |      b      | c  |  DEM 7    |
     +----+--------------+-------------+----+-----------+
     sizeof = 24, du lieu 10 byte, dem 14 byte


struct Tot { double b; char a; char c; };

byte:  0 .. 7      8    9    10 .. 15
     +-----------+----+----+-----------+
     |     b     | a  | c  |   DEM 6   |
     +-----------+----+----+-----------+
     sizeof = 16, du lieu 10 byte, dem 6 byte


Tiet kiem 8 byte tren 24, tuc 33 phan tram.
Voi mang mot trieu phan tu: tiet kiem 8 MB.

#Quy tắc sắp xếp

Thứ tự tự nhiên
typedef struct {
    bool     hoat_dong;      /* 1 */
    char     ten[32];        /* 32 */
    double   diem;           /* 8, can chinh 8 */
    char     lop[8];         /* 8 */
    int      ma;             /* 4 */
    bool     tot_nghiep;     /* 1 */
    long     ngay_sinh;      /* 4 hoac 8 */
} SinhVien;

/* Bo cuc tren x86-64 Linux, noi long la 8 byte:
     hoat_dong  0        1 byte
     ten        1..32    32 byte
     DEM        33..39   7 byte      <- lo trong
     diem       40..47   8 byte
     lop        48..55   8 byte
     ma         56..59   4 byte
     tot_nghiep 60       1 byte
     DEM        61..63   3 byte      <- lo trong
     ngay_sinh  64..71   8 byte
     sizeof = 72, du lieu 62 byte, dem 10 byte */
Sắp theo căn chỉnh giảm dần
typedef struct {
    double   diem;           /* 8, can chinh 8 */
    long     ngay_sinh;      /* 8 */
    char     ten[32];        /* 32, can chinh 1 */
    char     lop[8];         /* 8 */
    int      ma;             /* 4 */
    bool     hoat_dong;      /* 1 */
    bool     tot_nghiep;     /* 1 */
} SinhVien;

/* Bo cuc:
     diem        0..7     8 byte
     ngay_sinh   8..15    8 byte
     ten        16..47   32 byte
     lop        48..55    8 byte
     ma         56..59    4 byte
     hoat_dong  60        1 byte
     tot_nghiep 61        1 byte
     DEM        62..63    2 byte
     sizeof = 64, du lieu 62 byte, dem 2 byte

   72 xuong 64. Tren mot trieu ban ghi: tiet kiem 8 MB. */

#Đo trên mảng lớn

Đo bộ nhớ và tốc độ duyệt
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

typedef struct { char a; double b; char c; } Kem;
typedef struct { double b; char a; char c; } Tot;

enum { N = 5000000 };

static double duyet_kem(const Kem *ds) {
    double t = 0;
    for (size_t i = 0; i < N; ++i) t += ds[i].b;
    return t;
}

static double duyet_tot(const Tot *ds) {
    double t = 0;
    for (size_t i = 0; i < N; ++i) t += ds[i].b;
    return t;
}

int main(void) {
    printf("Kem: %d byte moi phan tu, %.1f MB cho %d phan tu\n",
           (int)sizeof(Kem), (double)sizeof(Kem) * N / 1048576.0, N);
    printf("Tot: %d byte moi phan tu, %.1f MB cho %d phan tu\n",
           (int)sizeof(Tot), (double)sizeof(Tot) * N / 1048576.0, N);

    Kem *a = calloc(N, sizeof *a);
    Tot *b = calloc(N, sizeof *b);
    if (a == NULL || b == NULL) return 1;

    clock_t t0 = clock();
    double s1 = duyet_kem(a);
    clock_t t1 = clock();
    double s2 = duyet_tot(b);
    clock_t t2 = clock();

    printf("duyet Kem: %.0f ms\n", (double)(t1 - t0) * 1000 / CLOCKS_PER_SEC);
    printf("duyet Tot: %.0f ms\n", (double)(t2 - t1) * 1000 / CLOCKS_PER_SEC);
    printf("(%.0f %.0f)\n", s1, s2);

    free(a); free(b);
    return 0;
}

#Khi nào thứ tự khai báo quan trọng hơn

Kế thừa thủ công: trường đầu tiên là bắt buộc
typedef struct { int loai; } CoSo;

typedef struct {
    CoSo co_so;              /* PHAI la truong dau tien */
    int  ban_kinh;
} HinhTron;

typedef struct {
    CoSo co_so;              /* PHAI la truong dau tien */
    int  rong, cao;
} HinhChuNhat;

void ve(CoSo *h) {
    switch (h->loai) {
        case 1: ve_tron((HinhTron *)h); break;
        case 2: ve_chu_nhat((HinhChuNhat *)h); break;
    }
}

HinhTron t = { { 1 }, 5 };
ve((CoSo *)&t);              /* HOP LE: con tro toi struct tro toi
                                thanh vien dau tien */

/* Neu ban sap xep lai va dua co_so xuong duoi thi phep ep kieu
   nay thanh UB, va chuong trinh doc sai truong loai.

   Nen: khi dung mau nay, THEM chu thich va _Static_assert: */

_Static_assert(offsetof(HinhTron, co_so) == 0, "co_so phai o dau");

#Để công cụ làm giúp

# 1. pahole -R de xuat thu tu toi uu
$ pahole -R --reorganize duan.o

struct SinhVien {
        double     diem;      /*  0  8 */
        long       ngay_sinh; /*  8  8 */
        char       ten[32];   /* 16 32 */
        ...
        /* size: 64, cachelines: 1 */
        /* saved 8 bytes! */

# 2. pahole --packable liet ke moi struct co the nen lai
$ pahole --packable duan.o
SinhVien           72      64        8
KetNoi            104      96        8
CauHinh            48      40        8
# ten, kich thuoc hien tai, kich thuoc toi uu, tiet kiem duoc

# 3. clang dump bo cuc
$ clang -Xclang -fdump-record-layouts -c bocuc.c
*** Dumping AST Record Layout
         0 | struct Kem
         0 |   char a
         8 |   double b
        16 |   char c
           | [sizeof=24, align=8]

# 4. GDB, da co san moi noi
(gdb) ptype/o struct SinhVien

Tự làm thử

  1. Đo sizeof của Kem và Tot và vẽ bản đồ byte của cả hai.
  2. Sắp xếp lại một struct trong dự án của bạn và đo mức tiết kiệm.
  3. Cấp phát năm triệu phần tử ở cả hai bố cục và đo thời gian duyệt.
  4. Chạy pahole --packable trên một dự án và tìm struct lãng phí nhất.
  5. Viết macro IN_TRUONG và dùng nó để in bố cục ba struct.
  6. Thêm _Static_assert chốt trường đầu tiên cho một struct dùng kế thừa thủ công.

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

  • { char; double; char; } là 24 byte, { double; char; char; } là 16 byte. Cùng dữ liệu, khác thứ tự.
  • Quy tắc là sắp theo căn chỉnh giảm dần, và mảng char có căn chỉnh 1 dù kích thước lớn.
  • Lợi ích tốc độ đến từ việc nhiều phần tử vào chung một dòng bộ đệm, và thường lớn hơn lợi ích bộ nhớ.
  • Bốn trường hợp không được sắp xếp lại: struct công khai, thanh ghi phần cứng, kế thừa thủ công, và khi thứ tự có nghĩa.
  • pahole -R đề xuất thứ tự tối ưu, và pahole --packable liệt kê mọi struct đáng sửa.