Tìm hiểu về Structure Alignment trong lập trình c/c++

Mỗi một kiểu dữ liệu đều có một sự căn chỉnh dữ liệu tương ứng, việc căn chỉnh này được quyết định bởi kiến trúc CPU chứ không phải là ngôn ngữ lập trình. Căn chỉnh các phần tử của dữ liệu cho phép CPU lấy dữ liệu từ bộ nhớ một cách hiệu quả. Do đó cải thiện hiệu năng (performance) của chương trình. Trình biên dịch của ngôn ngữ lập trình khi biên dịch chương trình sẽ cố gắng căn chỉnh các phần tử dữ liệu để cung cấp hiệu năng tối ưu. 

Trong phạm vi bài viết này chúng ta sẽ cùng tìm hiểu xem trình biên dịch C/C++ thực hiện việc căn chỉnh dữ liệu của structure như thế nào.

Memory access granualarity

Chúng ta có thể coi bộ nhớ đơn giản chỉ là một mảng các bytes xếp liên tiếp nhau như sau:

 

Hình 1 – Bộ nhớ dưới con mắt của lập trình viên

Tuy nhiên, trên thực tế thì CPU không đọc dữ liệu (từ memory) hay ghi dữ liệu (vào memory) theo từng byte. Thay vào đó nó truy cập vào memory theo từng block một, kích thước của mỗi block có thể là 2, 4, 8, 16 hoặc 32 bytes. Kích thước của mỗi block được gọi là memory access granularity” của CPU. Những địa chỉ là vị trí bắt đầu của một block (có giá trị chia hết cho kích thước của một block) được goi là “aligned address”. Những địa chỉ không nằm ở vị trí bắt đầu của một block (có giá trị không chia hết cho kích thước của một block) được gọi là “unaligned address”. Ví dụ: với một CPU đọc dữ liệu từ memory theo block 4 bytes thì các địa chỉ 0, 4, 8 là aligned address; các địa chỉ 1, 2, 3 là unaligned address.

 

Hình 2 – Bộ nhớ dưới con mắt của CPU

Nếu bạn không hiểu và xử lý các vấn đề liên quan đến căn chỉnh dữ liệu (data alignment) trong khi viết chương trình thì chương trình của bạn có thể sẽ gặp phải các vấn đề sau (sắp xếp theo độ nghiêm trọng tăng dần) 

  • Chương trình chạy chậm hơn.
  • Chương trình bị treo.
  • Hệ điều hành sụp đổ (crash).
  • Chương trình âm thầm chạy sai logic và gây ra hậu quả nghiêm trọng.

Data Structure Alignment trong C/C++

Để tránh cho CPU phải thực hiện thêm các xử lý khi truy cập unaligned addresses thì trình biện dịch C/C++ tự động thực hiện alignment data trong quá trình biên dịch, đảm bảo cho CPU luôn truy cập dữ liệu từ aligned addresses.

Mặc dù trình biên dịch thường phân bổ các thành phần dữ liệu riêng lẻ trên các địa chỉ đã được căn chỉnh, nhưng các cấu trúc dữ liệu (data structure) thường có nhiều phần tử dữ liệu thuộc các kiểu dữ liệu khác nhau với các giá trị căn chỉnh (alignment requirement) khác nhau. Trình biên dịch sẽ cố gắng duy trì việc căn chỉnh của các phần tử dữ liệu bằng cách chèn các ô nhớ không sử dụng giữa các phần tử. Kỹ thuật này được gọi là Padding”. Ngoài ra, trình biện dịch cũng thực hiện căn chỉnh toàn bộ structure bằng cách bổ sung padding vào cuối của structure, kỹ thuật này gọi là “Tail Padding”. Điều này làm cho mỗi phần tử của một mảng các structure được căn chỉnh hợp lý.

 

Padding sẽ được chèn vào trong structure khi một phần tử của structure được theo sau bởi một phần tử khác có giá trị căn chỉnh lớn hơn hoặc ở phần cuối của structure. Padding sẽ được thêm vào để căn chỉnh vị trí của các phần tử dữ liệu theo nguyên tắc sau:

  • Khoảng cách từ một phần tử dữ liệu trong structure đến đầu của structure (tính theo byte) phải chia hết cho giá trị căn chỉnh của phần tử dữ liệu đó. Ví dụ: một phần tử dữ liệu có kiểu int trong structure thì khoảng cách từ phần tử đó đến đầu structure phải chia hết cho 4.
  • Kích thước của toàn bộ structure phải đảm bảo chia hết cho giá trị căn chỉnh lớn nhất trong structure đó.

Thay đổi thứ tự của các phần tử dữ liệu trong một structure có thể làm thay đổi lượng padding cần thiết để duy trì sự căn chỉnh.

Do đó điều này giải thích tại sao trong khái niệm cấu trúc thêm vào trong c/c++, kích thước của kiểu cấu trúc sẽ không giống như chúng ta nghĩ. Xem ví dụ dưới đây:

Trong đó: Kiểu dữ liệu int có kích thước là 4 byte, char có kích thước 1 byte và float có kích thước 4 byte trong lập trình c/c++ với hệ điều hành 32 bit.

Do vậy nếu bình thường kích thước của cấu trúc theo cách tính của chúng ta sẽ là: 4+4+1+1+4 = 14 byte. Tuy nhiên như thế là không đúng mà phải tính như sau:

4 + 4 + 1 + 1 + 2 (địa chỉ rỗng chèn vào) + 4 = 16 byte.

Chúng ta cùng tìm hiểu 2 kiểu cấu trúc trong chương trình sau:

Kết quả của chương trình khi chạy trên máy tính của tôi như sau:

Giải thích kích thước của kiểu structure1 như sau:

  • Kích thước của id1 là 4 byte
  • Kích thước của id2 là 4 byte
  • Kích thước của name là 1 byte
  • Kích thước của c là 1 byte, kiểu float là 4 byte
  • Trong đó sẽ chèn 2 ô nhớ sau ô nhớ chứa biến c để biến float sẽ từ ô nhớ ở vị trí 12 như vậy tổng số kích thước của kiểu struct trên là: 4 + 4 + 1 + 1 + 2(empty) + 4 = 16 byte

Các bạn có thể xem hình minh họa của kiểu structure1 như sau:

Giải thích kích thước của kiểu structure2 như sau:

  • Kích thước của id1 là 4 byte
  • Kích thước của name là 1 byte
  • Kích thước của id2 là 4 byte
  • Kích thước của c là 1 byte, kiểu float là 4 byte
  • Trong đó sẽ chèn 3 ô nhớ rỗng sau ô nhớ chứa biến name và chèn 3 ô nhớ rỗng sau ô nhớ chứa biến c như vậy tổng số kích thước của kiểu struct là: 4 + 1 + 3 (empty) + 4 + 1 + 3(empty) + 4 = 20 byte

Các bạn có thể xem hình minh họa của kiểu structure1 như sau:

(Tham khảo bài viết thêm của: fresh2refresh và cppdeveloper)

Hy vọng qua bài viết này sẽ giúp các bạn hiểu rõ hơn về cách căn chỉnh, xử lý dữ liệu của CPU trong máy tính cũng như cách thức căn chỉnh của trình biên dịch trong lập trình c/c++. Bên cạnh đó các bạn muốn tham gia khóa học kinh nghiệm của chuyên gia qua dự án thực tế của Stanford có thể tham khảo thêm: tại đây

 

=============================
☎ STANFORD – ĐÀO TẠO VÀ PHÁT TRIỂN CÔNG NGHỆ
Hotline: 0963 723 236 – 0866 586 366
Website: https://stanford.com.vn
Facebook:
Youtube:

Nhận xét