Hai struct có đúng cùng các field — một int64 và hai bool — nhưng một cái tốn 24 byte, cái kia chỉ 16 byte. Khác biệt duy nhất là thứ tự khai báo field. Đây không phải chuyện lý thuyết: với cấu trúc dữ liệu cấp phát hàng triệu lần, thứ tự field dở có thể phí hàng chục MB. Bài này đo chính xác cơ chế đằng sau — alignment và padding — và cách sắp field để struct gọn nhất mà không đổi một dòng logic.
Alignment: vì sao có byte đệm
Mỗi kiểu dữ liệu có một yêu cầu căn lề (alignment): địa chỉ bộ nhớ của nó phải chia hết cho một số nhất định. Trên máy 64-bit, int64 căn lề 8 (phải nằm ở địa chỉ chia hết cho 8), int32 căn lề 4, bool/byte căn lề 1. Lý do là phần cứng: CPU đọc dữ liệu căn lề trong một thao tác, còn dữ liệu lệch lề có thể cần nhiều thao tác hoặc bị cấm.
Vì field trong struct được đặt theo đúng thứ tự khai báo, trình biên dịch phải chèn byte đệm (padding) giữa các field để mỗi field rơi vào địa chỉ căn lề đúng. Thứ tự field dở tạo ra nhiều đệm phí.

Hình 1: Cùng ba field. Xau (bool-int64-bool) xen kẽ nên int64 cần 7 byte đệm trước nó → 24 byte. Tot (int64-bool-bool) gom bool lại → 16 byte. Sơ đồ byte cho thấy đệm phí.
Đo thật: cùng field, khác thứ tự, khác kích thước
Dùng unsafe.Sizeof, unsafe.Alignof, và unsafe.Offsetof để soi bố cục thực:
type Xau struct { a bool; b int64; c bool } // xen kẽ
type Tot struct { b int64; a bool; c bool } // lớn trước
fmt.Println(unsafe.Sizeof(Xau{}), unsafe.Sizeof(Tot{}))
fmt.Println(unsafe.Offsetof(Xau{}.b)) // vị trí field b

Hình 2: Xau = 24 byte (offset a=0, b=8, c=16). Tot = 16 byte (offset b=0, a=8, c=9). Chênh 8 byte, 33% chỉ nhờ đổi thứ tự — thành 228 MB so với 152 MB ở 10 triệu struct.
Đọc offset là thấy rõ chuyện gì xảy ra:
Xau(24 byte):aở offset 0 (1 byte), nhưngb(int64) cần offset chia hết 8 nên phải nhảy tới offset 8 — 7 byte đệm phí ở giữa.cở offset 16, rồi cả struct phải căn lề 8 nên thêm 7 byte đệm cuối. Tổng đệm phí: 14 byte trên 24.Tot(16 byte):bở offset 0 (int64 vừa khít),aở offset 8,cở offset 9 (hai bool gom liền nhau), rồi 6 byte đệm cuối để đạt 16. Đệm ít hơn hẳn.
Cùng ba field, cùng dữ liệu, chỉ khác thứ tự khai báo — mà Xau tốn nhiều hơn 33%. Ở 10 triệu struct, đó là 228 MB so với 152 MB, chênh 76 MB.
Quy tắc sắp field
Nguyên tắc đơn giản: sắp field từ lớn tới nhỏ. Đặt các field 8-byte (int64, con trỏ, slice header, map, chan) trước, rồi 4-byte (int32, float32), rồi 2-byte, rồi 1-byte (bool, byte) cuối. Cách này gom các field nhỏ vào cuối, để chúng lấp đầy nhau thay vì mỗi cái tạo một khoảng đệm. Alignof của cả struct bằng alignment của field lớn nhất.
Ứng dụng thực tế
Tối ưu bộ nhớ miễn phí cho struct cấp phát nhiều. Nếu bạn có một struct dùng làm phần tử của slice/map lớn, node của cây, hay bản ghi trong hàng triệu, sắp lại field theo thứ tự lớn-tới-nhỏ cắt bộ nhớ ngay mà không đổi logic. Đây là một trong ít tối ưu "chỉ có lợi, không có hại".
Ít bộ nhớ = ít áp lực GC và tốt cache. Struct gọn hơn không chỉ tiết kiệm RAM: nó giảm lượng dữ liệu GC phải quét, và nhiều struct hơn khớp vào một cache line (64 byte) — cải thiện locality khi duyệt mảng struct. Lợi ích lan tỏa hơn con số byte thuần.
Dùng công cụ để phát hiện. go vet có kiểm fieldalignment (qua golang.org/x/tools), và lệnh fieldalignment -fix tự sắp lại field cho struct của bạn. Không cần soi tay từng struct — chạy công cụ trên các struct nóng.
Đánh đổi cần cân nhắc
Thứ tự field theo logic vs theo kích thước. Sắp field theo kích thước đôi khi làm struct khó đọc hơn (các field liên quan bị tách ra). Với struct ít cấp phát, tính dễ đọc quan trọng hơn vài byte — giữ thứ tự logic. Chỉ tối ưu alignment cho struct thật sự nóng về bộ nhớ.
Đừng chèn padding thủ công trừ khi có lý do. Đôi khi bạn cố ý thêm padding (ví dụ chống false sharing — bài sau). Nhưng thêm field _ [n]byte bừa bãi làm struct phình vô ích. Chỉ chèn padding khi đo được lợi ích cụ thể.
Kích thước struct còn phụ thuộc kiến trúc. Trên 32-bit, alignment và kích thước con trỏ khác (con trỏ 4 byte), nên bố cục struct đổi. unsafe.Sizeof cho kết quả đúng của kiến trúc đang biên dịch; đừng hardcode con số kích thước struct vào logic. Bài này đo trên arm64 64-bit.
Ba ý mang về
- Thứ tự field đổi kích thước struct vì trình biên dịch chèn byte đệm để căn lề: đo thật, cùng ba field (int64 + 2 bool) nhưng
bool-int64-booltốn 24 byte (14 byte đệm phí) so vớiint64-bool-booltốn 16 byte — chênh 33%. - Sắp field từ lớn tới nhỏ để giảm đệm: đặt field 8-byte trước, gom field 1-byte cuối để chúng lấp đầy nhau — dùng
unsafe.Sizeof/Offsetofđể soi, vàgo vet fieldalignment -fixđể tự sửa. - Tối ưu miễn phí ở quy mô lớn: 10 triệu struct chênh 76 MB chỉ nhờ đổi thứ tự khai báo — vừa tiết kiệm RAM, vừa giảm việc cho GC, vừa tốt cache; nhưng chỉ đáng làm cho struct nóng, struct thường thì ưu tiên thứ tự dễ đọc.
Bài tập làm thử
Bài 1 (đọc hiểu). Cho hai struct sau, đo được trên arm64 64-bit: unsafe.Sizeof(Xau{}) = 24, unsafe.Sizeof(Tot{}) = 16.
type Xau struct { a bool; b int64; c bool }
type Tot struct { b int64; a bool; c bool }
Bài viết cho biết offset của Xau là a=0, b=8, c=16; offset của Tot là b=0, a=8, c=9. Hãy giải thích từng byte đệm trong Xau xuất hiện ở đâu và vì sao.
Đáp án
Xau: a (bool, 1 byte, căn lề 1) nằm ở offset 0. b (int64, căn lề 8) cần offset chia hết cho 8, nên phải nhảy từ offset 1 lên offset 8 — chèn 7 byte đệm giữa a và b. b chiếm offset 8-15. c (bool) nằm ở offset 16, chiếm 1 byte (offset 16). Toàn bộ struct phải có kích thước là bội số của alignment lớn nhất trong struct (8, do int64), nên từ offset 17 tới 24 cần thêm 7 byte đệm cuối. Tổng: 7 (giữa) + 7 (cuối) = 14 byte đệm trên tổng 24 byte, tức phần dữ liệu thật chỉ 10 byte (1+8+1).
Tot gom b (int64) lên đầu (offset 0-7, khít, không cần đệm trước), rồi a (offset 8) và c (offset 9) là hai bool liền kề không cần đệm giữa chúng, cuối cùng đệm 6 byte (offset 10-15) để đạt bội số của 8. Tổng đệm chỉ 6 byte trên 16 byte.
Bài 2 (sửa lỗi). Một struct trong dự án được định nghĩa như sau và bị đánh giá là "tốn bộ nhớ dư thừa" khi review code. Áp dụng quy tắc "sắp field từ lớn tới nhỏ" từ bài viết để sắp lại field cho gọn nhất, không đổi field nào cả (chỉ đổi thứ tự).
type Diem struct {
XacNhan bool // 1 byte
ID int64 // 8 byte
ChuDong bool // 1 byte
Diem float64 // 8 byte
Cap int32 // 4 byte
}
Đáp án
Sắp theo thứ tự kích thước giảm dần (8 byte trước, rồi 4 byte, rồi 1 byte), gom các field nhỏ lại cuối để chúng lấp đầy nhau:
type Diem struct {
ID int64 // 8 byte, offset 0
Diem float64 // 8 byte, offset 8
Cap int32 // 4 byte, offset 16
XacNhan bool // 1 byte, offset 20
ChuDong bool // 1 byte, offset 21
// đệm cuối 2 byte để đạt offset 24 (bội số của 8)
}
Bản gốc (thứ tự xen kẽ bool-int64-bool-float64-int32) sẽ sinh nhiều đệm ở mỗi chỗ chuyển từ field nhỏ sang field lớn hơn; bản sắp lại nhóm hai int64/float64 (8 byte) liền nhau không cần đệm, int32 (4 byte) tiếp theo khít luôn (offset 16 chia hết 4), rồi hai bool gom liền nhau ở cuối chỉ cần đệm tối thiểu để căn lề tổng thể.
Bài 3 (vận dụng). Bài viết đo: 10 triệu struct Xau (24 byte) tốn 228 MB, còn Tot (16 byte) tốn 152 MB, chênh 76 MB (33%). Nếu một service của bạn giữ trong bộ nhớ một slice gồm 50 triệu bản ghi dạng struct tương tự Xau (bool-int64-bool), hãy ước tính RAM tiết kiệm được nếu đổi sang thứ tự field kiểu Tot, và giải thích vì sao đây được xem là "tối ưu chỉ có lợi, không có hại".
Đáp án
Theo tỷ lệ đo được (8 byte/struct chênh lệch ở quy mô 10 triệu → 76 MB), với 50 triệu bản ghi: 8 byte × 50.000.000 = 400.000.000 byte ≈ 381 MB tiết kiệm được (tính theo MiB) chỉ bằng cách đổi thứ tự khai báo field, không cần đổi logic xử lý nào. Bài viết gọi đây là tối ưu "chỉ có lợi, không có hại" vì: nó không thay đổi hành vi hay dữ liệu của struct (vẫn đủ 3 field, cùng giá trị), không cần thêm code, và ngoài việc giảm RAM trực tiếp còn có lợi ích lan tỏa: giảm lượng dữ liệu GC phải quét, và nhiều struct hơn khớp vào một cache line 64 byte giúp cải thiện locality khi duyệt mảng struct tuần tự.
Bài 4 (bẫy/đánh đổi). Bài viết nói: "Sắp field theo kích thước đôi khi làm struct khó đọc hơn... Với struct ít cấp phát, tính dễ đọc quan trọng hơn vài byte." Nêu một ví dụ tình huống cụ thể mà bạn KHÔNG nên áp dụng tối ưu alignment, và công cụ nào bài viết gợi ý dùng để tự động phát hiện/sửa cho các struct thực sự cần tối ưu.
Đáp án
Ví dụ: một struct cấu hình (Config) chỉ được tạo một lần khi khởi động chương trình, có vài chục field liên quan logic với nhau (ví dụ nhóm field về database đặt cạnh nhau, nhóm field về HTTP đặt cạnh nhau) để dễ đọc và bảo trì. Struct này không được cấp phát hàng loạt nên vài chục byte đệm dư không đáng kể so với lợi ích đọc hiểu — không nên sắp lại theo kích thước vì sẽ làm các field liên quan bị tách rời nhau trong code, khó review.
Công cụ bài viết gợi ý: go vet có kiểm fieldalignment (từ gói golang.org/x/tools), và lệnh fieldalignment -fix có thể tự động sắp lại field cho struct. Nên chạy công cụ này trên các struct thực sự "nóng" (cấp phát nhiều, ví dụ node cây, phần tử slice/map lớn) thay vì soi tay từng struct hay áp dụng tràn lan cho mọi struct.
Phần sau ta xét mặt trái của việc nhồi dữ liệu gần nhau: Phần sau mổ xẻ false sharing và cache line — vì sao hai biến gần nhau bị nhiều lõi tranh chấp có thể làm chậm chương trình song song, và khi nào cần thêm padding để tách chúng.