Memory Alignment là gì?

Trong Go, size thực tế của struct không chỉ là tổng size các field. Compiler sẽ chèn padding để bảo đảm memory alignment, nên thứ tự khai báo field có thể làm tổng size thay đổi. Sắp xếp field hợp lý giúp giảm lãng phí bộ nhớ và đôi khi cải thiện hiệu quả cache.
Memory Alignment là gì?

Dẫn nhập

Ta có bao giờ để ý tới size của class và struct chưa? Giả dụ ta có struct S như sau, bạn đọc đoán xem size của struct S là bao nhiêu?

type S struct {
  a int8
  b int64
  c int32
}

Nếu ta dùng theo quy chiếu thông thường về size của golang, ta có thể dễ dàng tính được size của S1 + 8 + 4 = 13 bytes. Điều đó không hề sai về mặt lý thuyết, chỉ là compiler của các ngôn ngữ không làm thế mà thôi. Thật tế, nếu dùng lệnh unsafe.Sizeof(S{}) thì kết quả sẽ là 24 bytes. Tại sao lại có kết quả lạ thường như thế?

Bảng 1: Kích thước của các kiểu dữ liệu trong Go.

Type Size (bytes)
byte, uint8, int8 1
uint16, int16 2
uint32, int32, float32 4
uint64, int64, float64, complex64 8
complex128 16

Memory Alignment

Trong máy tính kiến trúc 32 bit thì mỗi cycle của CPU khi load lên register thông thường sẽ là 4 bytes.

Đọc từ địa chỉ vị trí 0x0

Giả sử ta lại có kiểu dữ liệu int32 thì sẽ chiếm 2 ô nhớ trong RAM. Và máy tính vẫn hoạt động tốt nếu biến có kiểu int32 nằm trong đoạn 0x0 -> 0x2.

Đọc biến int32 từ vị trí 0x0

Nhưng câu chuyện phức tạp sẽ xảy ra nếu như biến bắt đầu nằm ở vị trí 0x3. Khi đó, để load biến thì CPU tốn 2 cycle để load dữ liệu, cắt, ghép lại để được biến kiểu int32 đó.

Do đó, các ngôn ngữ sẽ tự thêm các bytes trống để làm sao khi đọc tốn số lượng cycle ít nhất có thể, và được gọi là padding.

💡
Để tận dụng tối đa các lệnh của CPU và đạt được performance tốt nhất, thì địa chỉ (bắt đầu) của các kiểu dữ liệu T sẽ được cấp phát theo một bội số của một số nguyên N. Khi đó, N được gọi là alignment guarantee của kiểu dữ liệu T.

Alignment guarantee

Nói đơn giản, alignment guarantee cho biết: một giá trị của type này phải bắt đầu ở địa chỉ bộ nhớ nào.

💡
Trong Go, alignment có thể được kiểm tra bằng unsafe.Alignof(...).

Với kiểu dữ liệu cơ bản như int, byte, float,... thì alignment guarantee thường phụ thuộc vào kiến trúc máy tính như 64 bit hoặc 32 bit. Đa số là trùng với size của kiểu dữ liệu.

Bảng 2: Size và alignment guarantee của các kiểu dữ liệu trong Go trên kiến trúc 64 bit.

Kiểu dữ liệu (Type) Kích thước (Size - Bytes) Căn chỉnh (Alignment - Bytes)
bool, uint8, int8, byte 1 1
uint16, int16 2 2
uint32, int32, float32 4 4
string 16 (Header) 8
slice 24 (Header) 8
interface 16 (Header) 8
pointer (con trỏ) 8 8
struct{} (struct rỗng) 0 1

Go cũng có đưa ra một số đặc tả như sau:

  • For a variable x of any type: unsafe.Alignof(x) is at least 1.
  • For a variable x of struct type: unsafe.Alignof(x) is the largest of all the values unsafe.Alignof(x.f) for each field f of x, but at least 1.
  • For a variable x of array type: unsafe.Alignof(x) is the same as the alignment of a variable of the array's element type.
💡
Do đó, với struct thì alignment guarantee là alignment guarantee của field lớn nhất, nếu không có thì bằng 1.

Thực nghiệm

Giả sử ta có kiểu dữ liệu S1 như sau:

type S1 struct {
  a int8
  // 7 bytes sẽ được padding tại đây
  b int64
  c int16
  // 6 bytes được padding tại đây
}

S1 sẽ có size là 24:

  • Đầu tiên, struct có alignment guarantee là alignment guarantee của field lớn nhất: struct S1 sẽ có alignment guarantee của int64 là 8 bytes. Do đó, khoảng cách của field ab là 7 bytes.
  • Size của struct phải là bội số của alignment guarantee, ở đây là 8. Do đó, biến c cuối cùng sẽ được thêm 6 bytes cuối.

Tuy nhiên, vẫn là struct đó, nhưng ta khéo tổ chức dữ liệu hơn như sau:

type S2 struct {
  a int8
  // 1 bytes được padding tại đây
  c int16
  // 4 bytes được padding tại đây
  b int64
}

Bây giờ, size chỉ còn là 16 thôi và tiết kiệm được 8 bytes.

Kết luận

Việc ta không để ý tới memory alignment có thể dẫn đến một số vấn đề sau:

  • CPU có khả năng tốn nhiều ops hơn để đọc dữ liệu. Go compiler/runtime đã đảm bảo alignment bằng cách chèn padding và cấp phát ở địa chỉ phù hợp, nhưng không tự đổi thứ tự field.
  • Application chạy tốn RAM hơn. Vì phải padding nhiều hơn để có được vị trí đẹp.
  • Ít phần tử fit vào cache line hơn.
💡
Sắp xếp field từ lớn đến nhỏ thường giúp giảm padding, nhưng đó là heuristic chứ không phải luật tuyệt đối, vẫn cần cân bằng với readability và access pattern.

Tham khảo

Bình luận