SỬ DỤNG HIỆU QUẢ: DATA STRUCTURE ALIGNMENT

Trong bài viết này, tôi sẽ không giải thích về data structure alignment & padding bởi vì có rất nhiều bài viết trên mạng giải thích cặn kẽ và rất dễ hiểu cho vấn đề này. Mục đích của bài này là chia sẻ một số kinh nghiệm mà tôi đã gặp với data structure alignment, cũng như cách sử dụng struct trong C++ sao cho đúng và hiệu quả.

Xem thêm: Sự khác nhau giữa std::endl và “\n” trong C++

VẤN ĐỀ:

Bạn đang muốn parse (phân tích) một loại file nào đó, bạn biết được cấu trúc header của file đó và giờ bạn muốn đọc file đó lên để lấy từng giá trị trong header đó ra. Giả sử header đó có cấu trúc như sau:

File testFile.abc dưới định dạng Hex như sau:

Theo như cấu trúc file ở trên chúng ta có Header của file testFile.abc như sau (khi đọc lên thứ tự bị đảo ngược)

Bạn viết một chương trình để đọc file lên:

Hãy chạy thử chương trình trên xem có giống những gì chúng ta mong đợi không!?…Chạy thử đi đừng nhìn kết quả ở bên dưới nhé!…

Bang! Kết quả chương trình KHÔNG như mong đợi:

 

Tại sao signature và sizeOfData lại có kết quả đúng còn pointerToRawData  lại cho kết quả sai? Để ý một chút chúng ta sẽ thấy kết quả của pointerToRawData  bị dịch đi 2 byte. Yeah! Bạn nói đúng rồi đó, nguyên nhân là tại data alignment. Struct FileHeader đã được chèn thêm 2 byte chỗ short sizeOfData. Bên dưới là hình ảnh mô tả cấu trúc thực tế của struct FileHeader

Cấu trúc của struct FileHeader

Chương trình trên còn một bug nữa đó là bạn tưởng size của fileHeader là 10 byte nên đọc lên như thế này: inputFile.read((char *)&fileHeader, sizeof(fileHeader));. Tuy hiên, size thực tế của nó là 12 byte, hãy cực kỳ cẩn thận chỗ này, bởi vì code compile vẫn sạch error nhưng với một số trường hợp cụ thể khi run sẽ bị một lỗi rất lạ và bạn debug mãi không thể biết nguyên nhân do đâu (chúng ta sẽ bàn vấn đề này ở một bài viết khác).

Nguyên nhân đã rõ, vậy giờ giải quyết sao cho hợp lý!?

GIẢI PHÁP 1: ĐỌC LÊN TỪNG PHẦN

Có nghĩa là không đọc một lúc lên bởi như vậy nó sẽ bị dịch đi 2 byte. Chúng ta sẽ chỉ đọc tới phần sizeOfData, rồi dịch tới offset 6 đọc tiếp

Điểm tốt của giải pháp này là chúng ta vẫn giữ nguyên được struct FileHeader. Tuy nghiên đổi lại, chúng ta phải đọc file testFile.abc lên 2 lần với hàm inputFile.read(). Hàm read này cực kỳ tốn thời gian. Có thể đối với một chương trình nhỏ bạn sẽ thấy đây không phải là vấn đề gì, nhưng đối với một chương trình lớn, multithread, một chương trình mà tốc độ là key factor thì việc tốn 2 lần đọc như vậy sẽ làm thời gian thực thi của chương trình chậm đi rất nhiều. Đó là chưa kể struct ví dụ ở trên chỉ là 1 struct nhỏ, nếu một struct lớn và phức tạp hơn thì chúng ta phải chia ra đọc nhiều lần hơn nữa.

GIẢI PHÁP 2: THAY ĐỔI struct FileHeader

Chúng ta sẽ thử thay đổi struct FileHeader lại một chút, tất nhiên vẫn phải giữ được nguyên định nghĩa ban đầu của FileHeader về offset, size. Vậy giờ phải thay đổi như nào để có thể đọc lên một lần được? Phải khai báo biến bên trong struct FileHeader làm sao để nó không bị chèn thêm byte vô nữa?

Để struct không bị chèn thêm byte vô, chúng ta thử đổi khai báo từ int pointerToRawData sang char pointerToRawData[4]. Lúc này, chỗ short sizeOfData sẽ không bị chèn thêm 2 byte rỗng nữa. Lúc này cấu trúc thực tế của FileHeader sẽ giống như bên dưới

 

Data alignment of fileHeader

Sử dụng lại code ở đầu bài với một chút chỉnh sửa như sau

Chúng ta đã giải quyết được vấn đề tốn resouce vì đọc file lên nhiều lần bằng hàm read(). Tuy nhiên cấu trúc của FileHeader đã bị thay đổi. Có 2 vấn đề ở đây. Thứ nhất, nếu muốn sử dụng được pointerToRawData bạn phải làm một bước nữa là chuyển đổi giá trị từ mảng char[] sang một số nguyên kiểu int. Thứ hai, giả sử struct FileHeader phức tạp hơn, nhiều member hơn nữa, thì lúc này bạn phải thay đổi hết kiểu biến của các member đằng sau sizeOfData làm sao cho hợp lý. Ví dụ:

Vậy phải làm sao để vừa giữ được cấu trúc của FileHeader vừa đọc file lên một cách tối ưu nhất?

GIẢI PHÁP 3: PRAGMA PACK

Thông thường struct sẽ được compiler pack (đóng gói) và padding (chèn thêm padding nếu chưa đủ) theo cấu trúc 4 byte. Để struct không bị padding hoặc padding đúng với số byte mong muốn: 1, 2, 4, 8, 16; chúng ta có thể sử dụng PRAGMA PACK. (#pragma pack là gì và sử dụng như thế nào các bạn tìm hiểu thêm trên mạng nhé).

Đối với trường hợp FileHeader trong bài viết, chúng ta sẽ pack lại 2 byte là hợp lý nhất. Thêm pragma pack và sử dụng lại đoạn code đọc file ở đầu bài:

Dòng code #pragma pack(pop) ngay sau khai báo struct FileHeader là để cho những đoạn code còn lại được pack với mặc định của compiler. Nếu bạn đang sử dụng VS có thể chịnh lại mặc định số byte pack: chuột phải vô project -> Properties -> Configuration Properties -> C/C++ -> Code Generation -> Struct Member Alignment.

Configure default struct alignment in Visual Studio

Với pragma pack chúng ta sẽ giữ nguyên được cấu trúc của FileHeader, đồng thời hạn chế được việc tiêu tốn resource vì đọc lên nhiều lần bằng hàm read(). Tuy nhiên, không phải pragma pack không có hạn chế. Việc nén dữ liệu như vậy sẽ dẫn đến việc truy cập với từng phần tử trong FileHeader sẽ phải tiêu tốn thêm một ít resouce. Nhưng nếu so sánh với việc phải đọc file lên nhiều lần, thì chi phí cho việc này “rẻ” hơn, nên đối với trường hợp trong bài viết này, pragma pack là giải pháp thay thế hoàn toàn phù hợp và hiểu quả để ngăn chặn hạn chế của data alignment.

KẾT

Các bạn thấy đấy, chỉ là một vấn đề rất phổ biến và cơ bản của lập trình là data alignment nhưng nếu mổ sẻ ra sẽ rất nhiều thứ mà chúng ta phải xem xét. Nếu bạn không để ý kỹ, những bug tiềm ẩn mà data alignment để lại sẽ rất khó lường và khó debug. Ngay cả những giải pháp để thay thế không phải lúc nào cũng là hoàn hảo. Hãy là một developer cẩn thận nhé!

ĐỌC THÊM

Chia sẻ data structure alignment

Lý Do Tồn Tại Data Alignment (Struct Alignment, Memory Alignment)

#pragma pack

Khi nào không nên sử dụng pragma pack

Bài viết liên quan