Kết cấu đệm và đóng gói


209

Xem xét:

struct mystruct_A
{
   char a;
   int b;
   char c;
} x;

struct mystruct_B
{
   int b;
   char a;
} y;

Kích thước của các cấu trúc tương ứng là 12 và 8.

Là những cấu trúc đệm hoặc đóng gói?

Khi nào đệm hoặc đóng gói diễn ra?



24
Nghệ thuật đóng gói cấu trúc C bị mất - catb.org/esr/structure-packing
Paolo

paddinglàm cho mọi thứ lớn hơn packinglàm cho mọi thứ nhỏ hơn Hoàn toàn khác biệt.
smwikipedia

Câu trả lời:


264

Việc đệm sắp xếp các thành viên cấu trúc theo ranh giới địa chỉ "tự nhiên" - giả sử, intcác thành viên sẽ có giá trị bù trừ, mod(4) == 0trên nền tảng 32 bit. Đệm được bật theo mặc định. Nó chèn các "khoảng trống" sau vào cấu trúc đầu tiên của bạn:

struct mystruct_A {
    char a;
    char gap_0[3]; /* inserted by compiler: for alignment of b */
    int b;
    char c;
    char gap_1[3]; /* -"-: for alignment of the whole struct in an array */
} x;

Mặt khác, việc đóng gói ngăn không cho trình biên dịch thực hiện phần đệm - điều này phải được yêu cầu rõ ràng - theo GCC __attribute__((__packed__)), vì vậy, như sau:

struct __attribute__((__packed__)) mystruct_A {
    char a;
    int b;
    char c;
};

sẽ tạo ra cấu trúc kích thước 6trên kiến ​​trúc 32 bit.

Mặc dù vậy, một lưu ý - truy cập bộ nhớ không được phân bổ chậm hơn đối với các kiến ​​trúc cho phép nó (như x86 và amd64), và bị cấm rõ ràng đối với các kiến ​​trúc căn chỉnh nghiêm ngặt như SPARC.


2
Tôi tự hỏi: việc cấm bộ nhớ không được sắp xếp trên tia lửa có nghĩa là nó không thể xử lý các mảng byte thông thường? Cấu trúc đóng gói như tôi biết chủ yếu được sử dụng trong việc truyền (tức là kết nối mạng) dữ liệu, khi bạn cần truyền một mảng byte cho một cấu trúc và chắc chắn rằng một mảng phù hợp với các trường cấu trúc. Nếu tia lửa không thể làm điều đó, làm thế nào những người đó làm việc?!
Hi-Angel

14
Đó chính xác là lý do tại sao, nếu bạn nhìn vào bố cục tiêu đề IP, UDP và TCP, bạn sẽ thấy rằng tất cả các trường số nguyên được căn chỉnh.
Nikolai Fetissov

17
"Nghệ thuật đóng gói cấu trúc C bị mất" giải thích việc đệm và đóng gói ptimisations - catb.org/esr/structure-packing
Rob11311

3
Có thành viên đầu tiên phải đến đầu tiên? Tôi nghĩ rằng sự hăng hái hoàn toàn phụ thuộc vào việc thực hiện và không thể dựa vào (ngay cả từ phiên bản này sang phiên bản khác).
mã allyour

4
+ allyourcode Tiêu chuẩn đảm bảo rằng thứ tự của các thành viên sẽ được giữ nguyên và thành viên đầu tiên sẽ bắt đầu ở mức 0 bù.
martinkunev

64

( Các câu trả lời trên đã giải thích lý do khá rõ ràng, nhưng dường như không hoàn toàn rõ ràng về kích thước của phần đệm, vì vậy, tôi sẽ thêm một câu trả lời theo những gì tôi học được từ The Lost Art of Architecture Packaging , nó đã phát triển để không giới hạn C, nhưng cũng áp dụng cho Go, Rust. )


Căn chỉnh bộ nhớ (cho struct)

Quy tắc:

  • Trước mỗi thành viên riêng lẻ, sẽ có phần đệm để làm cho nó bắt đầu tại một địa chỉ chia hết cho kích thước của nó.
    ví dụ: trên hệ thống 64 bit, intnên bắt đầu tại địa chỉ chia hết cho 4 và long8, shortcho 2.
  • charchar[]đặc biệt, có thể là bất kỳ địa chỉ bộ nhớ nào, vì vậy chúng không cần đệm trước chúng.
  • Đối với struct, ngoài nhu cầu căn chỉnh cho từng thành viên riêng lẻ, kích thước của toàn bộ cấu trúc sẽ được căn chỉnh theo kích thước chia hết cho kích thước của thành viên lớn nhất, bằng cách đệm ở cuối.
    ví dụ: nếu thành viên lớn nhất của struct longchia hết cho 8, intrồi 4, shortrồi 2.

Thứ tự của thành viên:

  • Thứ tự của thành viên có thể ảnh hưởng đến kích thước thực tế của struct, vì vậy hãy ghi nhớ điều đó. ví dụ stu_cstu_dví dụ dưới đây có cùng các thành viên, nhưng theo thứ tự khác nhau và dẫn đến kích thước khác nhau cho 2 cấu trúc.

Địa chỉ trong bộ nhớ (cho struct)

Quy tắc:

  • Hệ thống 64 bit
    Địa chỉ cấu trúc bắt đầu từ (n * 16)byte. ( Bạn có thể thấy trong ví dụ bên dưới, tất cả các địa chỉ hex được in của các cấu trúc kết thúc bằng 0. )
    Lý do : thành viên cấu trúc riêng lẻ lớn nhất có thể là 16 byte ( long double).
  • (Cập nhật) Nếu một cấu trúc chỉ chứa mộtcharthành viên, địa chỉ của nó có thể bắt đầu tại bất kỳ địa chỉ nào.

Không gian trống :

  • Không gian trống giữa 2 cấu trúc có thể được sử dụng bởi các biến không có cấu trúc có thể phù hợp.
    Ví dụ như test_struct_address()bên dưới, biến xnằm giữa struct liền kề gh.
    Cho dù xđược khai báo, hđịa chỉ sẽ không thay đổi, xchỉ sử dụng lại không gian trống gbị lãng phí.
    Trường hợp tương tự cho y.

Thí dụ

( đối với hệ thống 64 bit )

memory_align.c :

/**
 * Memory align & padding - for struct.
 * compile: gcc memory_align.c
 * execute: ./a.out
 */ 
#include <stdio.h>

// size is 8, 4 + 1, then round to multiple of 4 (int's size),
struct stu_a {
    int i;
    char c;
};

// size is 16, 8 + 1, then round to multiple of 8 (long's size),
struct stu_b {
    long l;
    char c;
};

// size is 24, l need padding by 4 before it, then round to multiple of 8 (long's size),
struct stu_c {
    int i;
    long l;
    char c;
};

// size is 16, 8 + 4 + 1, then round to multiple of 8 (long's size),
struct stu_d {
    long l;
    int i;
    char c;
};

// size is 16, 8 + 4 + 1, then round to multiple of 8 (double's size),
struct stu_e {
    double d;
    int i;
    char c;
};

// size is 24, d need align to 8, then round to multiple of 8 (double's size),
struct stu_f {
    int i;
    double d;
    char c;
};

// size is 4,
struct stu_g {
    int i;
};

// size is 8,
struct stu_h {
    long l;
};

// test - padding within a single struct,
int test_struct_padding() {
    printf("%s: %ld\n", "stu_a", sizeof(struct stu_a));
    printf("%s: %ld\n", "stu_b", sizeof(struct stu_b));
    printf("%s: %ld\n", "stu_c", sizeof(struct stu_c));
    printf("%s: %ld\n", "stu_d", sizeof(struct stu_d));
    printf("%s: %ld\n", "stu_e", sizeof(struct stu_e));
    printf("%s: %ld\n", "stu_f", sizeof(struct stu_f));

    printf("%s: %ld\n", "stu_g", sizeof(struct stu_g));
    printf("%s: %ld\n", "stu_h", sizeof(struct stu_h));

    return 0;
}

// test - address of struct,
int test_struct_address() {
    printf("%s: %ld\n", "stu_g", sizeof(struct stu_g));
    printf("%s: %ld\n", "stu_h", sizeof(struct stu_h));
    printf("%s: %ld\n", "stu_f", sizeof(struct stu_f));

    struct stu_g g;
    struct stu_h h;
    struct stu_f f1;
    struct stu_f f2;
    int x = 1;
    long y = 1;

    printf("address of %s: %p\n", "g", &g);
    printf("address of %s: %p\n", "h", &h);
    printf("address of %s: %p\n", "f1", &f1);
    printf("address of %s: %p\n", "f2", &f2);
    printf("address of %s: %p\n", "x", &x);
    printf("address of %s: %p\n", "y", &y);

    // g is only 4 bytes itself, but distance to next struct is 16 bytes(on 64 bit system) or 8 bytes(on 32 bit system),
    printf("space between %s and %s: %ld\n", "g", "h", (long)(&h) - (long)(&g));

    // h is only 8 bytes itself, but distance to next struct is 16 bytes(on 64 bit system) or 8 bytes(on 32 bit system),
    printf("space between %s and %s: %ld\n", "h", "f1", (long)(&f1) - (long)(&h));

    // f1 is only 24 bytes itself, but distance to next struct is 32 bytes(on 64 bit system) or 24 bytes(on 32 bit system),
    printf("space between %s and %s: %ld\n", "f1", "f2", (long)(&f2) - (long)(&f1));

    // x is not a struct, and it reuse those empty space between struts, which exists due to padding, e.g between g & h,
    printf("space between %s and %s: %ld\n", "x", "f2", (long)(&x) - (long)(&f2));
    printf("space between %s and %s: %ld\n", "g", "x", (long)(&x) - (long)(&g));

    // y is not a struct, and it reuse those empty space between struts, which exists due to padding, e.g between h & f1,
    printf("space between %s and %s: %ld\n", "x", "y", (long)(&y) - (long)(&x));
    printf("space between %s and %s: %ld\n", "h", "y", (long)(&y) - (long)(&h));

    return 0;
}

int main(int argc, char * argv[]) {
    test_struct_padding();
    // test_struct_address();

    return 0;
}

Kết quả thực hiện - test_struct_padding():

stu_a: 8
stu_b: 16
stu_c: 24
stu_d: 16
stu_e: 16
stu_f: 24
stu_g: 4
stu_h: 8

Kết quả thực hiện - test_struct_address():

stu_g: 4
stu_h: 8
stu_f: 24
address of g: 0x7fffd63a95d0  // struct variable - address dividable by 16,
address of h: 0x7fffd63a95e0  // struct variable - address dividable by 16,
address of f1: 0x7fffd63a95f0 // struct variable - address dividable by 16,
address of f2: 0x7fffd63a9610 // struct variable - address dividable by 16,
address of x: 0x7fffd63a95dc  // non-struct variable - resides within the empty space between struct variable g & h.
address of y: 0x7fffd63a95e8  // non-struct variable - resides within the empty space between struct variable h & f1.
space between g and h: 16
space between h and f1: 16
space between f1 and f2: 32
space between x and f2: -52
space between g and x: 12
space between x and y: 12
space between h and y: 8

Do đó, địa chỉ bắt đầu cho mỗi biến là g: d0 x: dc h: e0 y: e8

nhập mô tả hình ảnh ở đây


4
"Các quy tắc" thực sự đã làm cho nó rất rõ ràng, tôi không thể tìm thấy quy tắc đơn giản ở bất cứ đâu. Cảm ơn.
Pervez Alam

2
@PervezAlam Cuốn sách <The Lost Art of C Structure Packing>, giải thích các quy tắc khá tốt, thậm chí còn nghĩ rằng nó dài hơn một chút so với câu trả lời này. Cuốn sách có sẵn trực tuyến miễn phí: catb.org/esr/structure-packing
Eric Wang

Tôi sẽ thử, btw có bị giới hạn trong việc đóng gói Cấu trúc không? Chỉ tò mò là tôi thích lời giải thích trong cuốn sách.
Pervez Alam

1
@PervezAlam Đó là một cuốn sách rất ngắn, chủ yếu tập trung vào công nghệ giúp giảm dung lượng bộ nhớ của chương trình c, chỉ mất ít nhất vài ngày để đọc xong.
Eric Wang

1
@ValidusOculus Có, nó có nghĩa là 16 byte được căn chỉnh.
Eric Wang

44

Tôi biết câu hỏi này đã cũ và hầu hết các câu trả lời ở đây giải thích rất tốt về phần đệm, nhưng trong khi cố gắng tự hiểu nó, tôi đã hình dung ra một hình ảnh "trực quan" về những gì đang xảy ra.

Bộ xử lý đọc bộ nhớ trong "khối" có kích thước xác định (từ). Nói từ bộ xử lý dài 8 byte. Nó sẽ xem bộ nhớ như một hàng lớn gồm các khối xây dựng 8 byte. Mỗi khi nó cần lấy một số thông tin từ bộ nhớ, nó sẽ đến một trong những khối đó và lấy nó.

Biến liên kết

Như hình ảnh bên trên, không quan trọng Char ở đâu (dài 1 byte), vì nó sẽ nằm trong một trong những khối đó, yêu cầu CPU chỉ xử lý 1 từ.

Khi chúng ta xử lý dữ liệu lớn hơn một byte, như int 4 byte hoặc gấp đôi 8 byte, cách chúng được căn chỉnh trong bộ nhớ sẽ tạo ra sự khác biệt về số lượng từ mà CPU sẽ phải xử lý. Nếu các khối 4 byte được căn chỉnh theo cách chúng luôn khớp với bên trong một khối (địa chỉ bộ nhớ là bội số của 4) thì chỉ một từ sẽ phải được xử lý. Mặt khác, một đoạn 4 byte có thể có một phần của chính nó trên một khối và một phần trên khối khác, yêu cầu bộ xử lý xử lý 2 từ để đọc dữ liệu này.

Điều tương tự cũng áp dụng cho nhân đôi 8 byte, ngoại trừ bây giờ nó phải nằm trong địa chỉ bộ nhớ bội số của 8 để đảm bảo nó sẽ luôn ở trong một khối.

Điều này xem xét một trình xử lý văn bản 8 byte, nhưng khái niệm này áp dụng cho các kích thước khác của từ.

Phần đệm hoạt động bằng cách lấp đầy các khoảng trống giữa các dữ liệu đó để đảm bảo chúng được căn chỉnh với các khối đó, do đó cải thiện hiệu suất trong khi đọc bộ nhớ.

Tuy nhiên, như đã nêu trên các câu trả lời khác, đôi khi không gian quan trọng hơn hiệu suất. Có thể bạn đang xử lý nhiều dữ liệu trên máy tính không có nhiều RAM (có thể sử dụng không gian hoán đổi nhưng chậm hơn RẤT NHIỀU). Bạn có thể sắp xếp các biến trong chương trình cho đến khi hoàn thành phần đệm ít nhất (vì nó đã được minh họa rất nhiều trong một số câu trả lời khác) nhưng nếu điều đó không đủ, bạn có thể vô hiệu hóa phần đệm, đó là cách đóng gói .


3
Điều này không giải thích việc đóng gói cấu trúc nhưng nó minh họa việc căn chỉnh từ CPU khá độc đáo.
David Foerster

Bạn đã vẽ nó trong sơn? :-)
Ciro Santilli 郝海东 冠状 病 六四 事件

1
@ CiroSantilli709 抓捕 六四, đó là trên gimp, nhưng tôi đoán tôi đã tiết kiệm được một chút thời gian trên sơn mặc dù haha
IanC

1
Thậm chí tốt hơn kể từ khi nguồn mở (Y)
Ciro Santilli 冠状 病 六四 事件

21

Cấu trúc đóng gói ngăn chặn đệm cấu trúc, đệm được sử dụng khi căn chỉnh là vấn đề quan trọng nhất, đóng gói được sử dụng khi không gian quan trọng nhất.

Một số trình biên dịch cung cấp #pragmađể loại bỏ phần đệm hoặc để làm cho nó được đóng gói tới n số byte. Một số cung cấp từ khóa để làm điều này. Nói chung pragma được sử dụng để sửa đổi phần đệm cấu trúc sẽ có định dạng dưới đây (tùy thuộc vào trình biên dịch):

#pragma pack(n)

Ví dụ ARM cung cấp __packedtừ khóa để loại bỏ cấu trúc đệm. Đi qua hướng dẫn biên dịch của bạn để tìm hiểu thêm về điều này.

Vì vậy, một cấu trúc đóng gói là một cấu trúc không có đệm.

Cấu trúc đóng gói nói chung sẽ được sử dụng

  • để tiết kiệm không gian

  • để định dạng cấu trúc dữ liệu để truyền qua mạng bằng cách sử dụng một số giao thức (tất nhiên đây không phải là một cách thực hành tốt vì bạn cần phải
    xử lý tính cuối cùng)


5

Đệm và đóng gói chỉ là hai khía cạnh của cùng một điều:

  • đóng gói hoặc căn chỉnh là kích thước mà mỗi thành viên được làm tròn
  • phần đệm là không gian thêm được thêm vào để phù hợp với sự liên kết

Trong mystruct_Agiả sử liên kết mặc định là 4, mỗi thành viên được căn chỉnh trên bội số của 4 byte. Vì kích thước charlà 1, nên phần đệm cho aclà 4 - 1 = 3 byte trong khi không yêu cầu phần đệm int bnào là 4 byte. Nó hoạt động theo cùng một cách cho mystruct_B.


1

Cấu trúc đóng gói chỉ được thực hiện khi bạn nói rõ trình biên dịch của mình để đóng gói cấu trúc. Đệm là những gì bạn đang thấy. Hệ thống 32 bit của bạn đang đệm từng trường để căn chỉnh từ. Nếu bạn đã bảo trình biên dịch của mình đóng gói các cấu trúc, thì chúng sẽ lần lượt là 6 và 5 byte. Đừng làm vậy mặc dù. Nó không khả dụng và khiến trình biên dịch tạo mã chậm hơn (và đôi khi thậm chí là lỗi).


1

Không có buts về nó! Ai muốn nắm bắt chủ đề phải làm những việc sau,

  • Peruse Nghệ thuật mất cấu trúc đóng gói được viết bởi Eric S. Raymond
  • Nhìn vào ví dụ mã của Eric
  • Cuối cùng nhưng không kém phần quan trọng, đừng quên quy tắc sau đây về phần đệm rằng một cấu trúc được căn chỉnh theo các yêu cầu căn chỉnh của loại lớn nhất.

1

Quy tắc đệm:

  1. Mỗi thành viên của cấu trúc nên ở một địa chỉ chia hết cho kích thước của nó. Đệm được chèn giữa các phần tử hoặc ở cuối cấu trúc để đảm bảo quy tắc này được đáp ứng. Điều này được thực hiện để truy cập Bus dễ dàng và hiệu quả hơn bởi phần cứng.
  2. Đệm ở cuối cấu trúc được quyết định dựa trên kích thước của thành viên lớn nhất của cấu trúc.

Tại sao Quy tắc 2: Xem xét cấu trúc sau,

Cấu trúc 1

Nếu chúng ta tạo một mảng (gồm 2 cấu trúc) của cấu trúc này, cuối cùng sẽ không có phần đệm nào được yêu cầu:

Mảng Struct1

Do đó, kích thước của struct = 8 byte

Giả sử chúng ta đã tạo một cấu trúc khác như dưới đây:

Cấu trúc 2

Nếu chúng ta tạo ra một mảng của cấu trúc này, có 2 khả năng, về số lượng byte được yêu cầu ở cuối.

A. Nếu chúng ta thêm 3 byte ở cuối và căn chỉnh nó cho int và không dài:

Mảng Struct2 được liên kết với int

B. Nếu chúng ta thêm 7 byte ở cuối và căn chỉnh cho Long:

Mảng Struct2 được căn chỉnh theo chiều dài

Địa chỉ bắt đầu của mảng thứ hai là bội số của 8 (tức là 24). Kích thước của struct = 24 byte

Do đó, bằng cách căn chỉnh địa chỉ bắt đầu của mảng tiếp theo của cấu trúc với bội số của thành viên lớn nhất (nghĩa là nếu chúng ta tạo một mảng của cấu trúc này, địa chỉ đầu tiên của mảng thứ hai phải bắt đầu tại một địa chỉ là bội số của thành viên lớn nhất của struct. Ở đây, 24 (3 * 8)), chúng ta có thể tính toán số byte đệm được yêu cầu ở cuối.


-1

Căn chỉnh cấu trúc dữ liệu là cách dữ liệu được sắp xếp và truy cập trong bộ nhớ máy tính. Nó bao gồm hai vấn đề riêng biệt nhưng có liên quan: căn chỉnh dữ liệuđệm cấu trúc dữ liệu . Khi một máy tính hiện đại đọc từ hoặc ghi vào một địa chỉ bộ nhớ, nó sẽ thực hiện điều này trong các đoạn có kích thước từ (ví dụ: các đoạn 4 byte trên hệ thống 32 bit) hoặc lớn hơn. Căn chỉnh dữ liệu có nghĩa là đặt dữ liệu tại một địa chỉ bộ nhớ bằng với một số bội số của kích thước từ, làm tăng hiệu suất của hệ thống do cách CPU xử lý bộ nhớ. Để căn chỉnh dữ liệu, có thể cần phải chèn một số byte vô nghĩa giữa phần cuối của cấu trúc dữ liệu cuối cùng và phần đầu của phần tiếp theo, đó là phần đệm cấu trúc dữ liệu.

  1. Để căn chỉnh dữ liệu trong bộ nhớ, một hoặc nhiều byte trống (địa chỉ) được chèn (hoặc để trống) giữa các địa chỉ bộ nhớ được phân bổ cho các thành viên cấu trúc khác trong khi cấp phát bộ nhớ. Khái niệm này được gọi là đệm cấu trúc.
  2. Kiến trúc của bộ xử lý máy tính là cách nó có thể đọc 1 từ (bộ xử lý 4 byte trong 32 bit) từ bộ nhớ tại một thời điểm.
  3. Để sử dụng lợi thế này của bộ xử lý, dữ liệu luôn được căn chỉnh dưới dạng gói 4 byte dẫn đến chèn địa chỉ trống giữa địa chỉ của thành viên khác.
  4. Do khái niệm đệm cấu trúc này trong C, kích thước của cấu trúc luôn không giống như những gì chúng ta nghĩ.

1
Tại sao bạn cần liên kết đến cùng một bài viết 5 lần trong câu trả lời của bạn? Vui lòng chỉ giữ một liên kết đến ví dụ. Ngoài ra, vì bạn đang liên kết đến bài viết của mình, bạn cần tiết lộ sự thật đó.
Artjom B.
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.