Hiệu ứng gói #pragma


233

Tôi đã tự hỏi nếu ai đó có thể giải thích cho tôi những gì #pragma packtuyên bố tiền xử lý làm, và quan trọng hơn, tại sao một người muốn sử dụng nó.

Tôi đã kiểm tra trang MSDN , trang này cung cấp một số thông tin chi tiết, nhưng tôi hy vọng được nghe nhiều hơn từ những người có kinh nghiệm. Tôi đã nhìn thấy nó trong mã trước đây, mặc dù tôi dường như không thể tìm thấy ở đâu nữa.


1
Nó buộc một sự liên kết / đóng gói cụ thể của một cấu trúc, nhưng giống như tất cả các #pragmachỉ thị mà chúng được thực hiện được xác định.
dreamlax

A mod s = 0Trong đó A là địa chỉ và s là kích thước của kiểu dữ liệu; kiểm tra này nếu dữ liệu không được sắp xếp sai.
huyền thoại2k

Câu trả lời:


421

#pragma packhướng dẫn trình biên dịch để đóng gói các thành viên cấu trúc với sự liên kết cụ thể. Hầu hết các trình biên dịch, khi bạn khai báo một cấu trúc, sẽ chèn phần đệm giữa các thành viên để đảm bảo rằng chúng được căn chỉnh theo các địa chỉ phù hợp trong bộ nhớ (thường là bội số của kích thước loại). Điều này tránh được hình phạt hiệu suất (hoặc lỗi hoàn toàn) trên một số kiến ​​trúc liên quan đến việc truy cập các biến không được căn chỉnh chính xác. Ví dụ, đã cho các số nguyên 4 byte và cấu trúc sau:

struct Test
{
   char AA;
   int BB;
   char CC;
};

Trình biên dịch có thể chọn đặt cấu trúc ra trong bộ nhớ như thế này:

|   1   |   2   |   3   |   4   |  

| AA(1) | pad.................. |
| BB(1) | BB(2) | BB(3) | BB(4) | 
| CC(1) | pad.................. |

sizeof(Test)sẽ là 4 × 3 = 12, mặc dù nó chỉ chứa 6 byte dữ liệu. Trường hợp sử dụng phổ biến nhất cho #pragma(theo hiểu biết của tôi) là khi làm việc với các thiết bị phần cứng, nơi bạn cần đảm bảo rằng trình biên dịch không chèn phần đệm vào dữ liệu và mỗi thành viên tuân theo quy tắc trước đó. Với #pragma pack(1), cấu trúc trên sẽ được trình bày như thế này:

|   1   |

| AA(1) |
| BB(1) |
| BB(2) |
| BB(3) |
| BB(4) |
| CC(1) |

sizeof(Test)sẽ là 1 × 6 = 6.

Với #pragma pack(2), cấu trúc trên sẽ được trình bày như thế này:

|   1   |   2   | 

| AA(1) | pad.. |
| BB(1) | BB(2) |
| BB(3) | BB(4) |
| CC(1) | pad.. |

sizeof(Test)sẽ là 2 × 4 = 8.

Thứ tự các biến trong struct cũng rất quan trọng. Với các biến được sắp xếp như sau:

struct Test
{
   char AA;
   char CC;
   int BB;
};

và với #pragma pack(2), cấu trúc sẽ được đặt ra như thế này:

|   1   |   2   | 

| AA(1) | CC(1) |
| BB(1) | BB(2) |
| BB(3) | BB(4) |

sizeOf(Test)sẽ là 3 × 2 = 6.


76
Nó có thể là giá trị thêm các nhược điểm của đóng gói. (truy cập đối tượng không được chỉ định là chậm trong trường hợp tốt nhất , nhưng sẽ gây ra lỗi trên một số nền tảng.)
jalf

11
Có vẻ như sự sắp xếp "hình phạt hiệu suất" được đề cập thực sự có thể là một lợi ích trên một số hệ thống danluu.com/3c-conflict .

6
@Pacerier Không hẳn. Bài đăng đó nói về một số căn chỉnh khá cực đoan (căn chỉnh trên ranh giới 4KB). CPU mong đợi sự sắp xếp tối thiểu nhất định cho các loại dữ liệu khác nhau, nhưng trong những trường hợp xấu nhất, trong trường hợp xấu nhất, căn chỉnh 8 byte (không tính các loại vectơ có thể yêu cầu căn chỉnh 16 hoặc 32 byte). Không căn chỉnh trên các ranh giới đó thường mang lại cho bạn một hiệu suất đáng chú ý (vì tải có thể phải được thực hiện dưới dạng hai thao tác thay vì một), nhưng loại này được căn chỉnh tốt hoặc không. Sự liên kết chặt chẽ hơn so với mua bạn không có gì (và đống đổ nát bộ nhớ cache sử dụng
jalf

6
Nói cách khác, một nhân đôi dự kiến ​​sẽ ở trên một ranh giới 8 byte. Đặt nó trên một ranh giới 7 byte sẽ làm giảm hiệu suất. Nhưng đặt nó trên một ranh giới 16, 32, 64 hoặc 4096 byte sẽ không mua gì cho bạn trên ranh giới 8 byte đã cho bạn. Bạn sẽ nhận được hiệu suất tương tự từ CPU, trong khi việc sử dụng bộ đệm kém hơn nhiều vì những lý do được nêu trong bài đăng đó.
jalf

4
Vì vậy, bài học không phải là "đóng gói là có lợi" (đóng gói vi phạm liên kết tự nhiên của các loại, do đó đau hiệu suất), nhưng đơn giản là 'không quá-align xa hơn những gì là cần thiết'
jalf

27

#pragmađược sử dụng để gửi tin nhắn không di động (như trong trình biên dịch này) đến trình biên dịch. Những việc như vô hiệu hóa một số cảnh báo và cấu trúc đóng gói là những lý do phổ biến. Vô hiệu hóa các cảnh báo cụ thể đặc biệt hữu ích nếu bạn biên dịch với các cảnh báo khi cờ lỗi được bật.

#pragma packcụ thể được sử dụng để chỉ ra rằng cấu trúc được đóng gói không nên có các thành viên của nó được căn chỉnh. Nó hữu ích khi bạn có một giao diện được ánh xạ bộ nhớ tới một phần cứng và cần có khả năng kiểm soát chính xác vị trí của các thành viên cấu trúc khác nhau. Nó đáng chú ý không phải là một tối ưu hóa tốc độ tốt, vì hầu hết các máy nhanh hơn nhiều trong việc xử lý dữ liệu được căn chỉnh.


17
Để hoàn tác sau đó, hãy làm điều này: gói #pragma (đẩy, 1) và #pragma gói (pop)
malhal

16

Nó báo cho trình biên dịch ranh giới để sắp xếp các đối tượng trong một cấu trúc. Ví dụ: nếu tôi có một cái gì đó như:

struct foo { 
    char a;
    int b;
};

Với một máy 32 bit thông thường, thông thường bạn sẽ "muốn" có 3 byte đệm giữa abdo đó bsẽ hạ cánh ở ranh giới 4 byte để tối đa hóa tốc độ truy cập của nó (và đó là điều thường xảy ra theo mặc định).

Tuy nhiên, nếu bạn phải khớp với cấu trúc được xác định bên ngoài mà bạn muốn đảm bảo trình biên dịch đưa ra cấu trúc của bạn chính xác theo định nghĩa bên ngoài đó. Trong trường hợp này, bạn có thể cung cấp cho trình biên dịch #pragma pack(1)để bảo nó không chèn bất kỳ phần đệm nào giữa các thành viên - nếu định nghĩa của cấu trúc bao gồm phần đệm giữa các thành viên, bạn chèn nó một cách rõ ràng (ví dụ, thông thường với các thành viên có tên unusedNhoặc ignoreN, hoặc một cái gì đó trên đó đặt hàng).


"Bạn thường" muốn "có 3 byte đệm giữa a và b để b sẽ hạ cánh ở ranh giới 4 byte để tối đa hóa tốc độ truy cập của nó" - làm thế nào để có 3 byte đệm tối đa hóa tốc độ truy cập?
Ashwin

8
@Ashwin: Đặt bở ranh giới 4 byte có nghĩa là bộ xử lý có thể tải nó bằng cách phát ra một tải 4 byte duy nhất. Mặc dù nó phụ thuộc phần nào vào bộ xử lý, nhưng nếu nó ở một ranh giới kỳ lạ thì rất có thể việc tải nó sẽ yêu cầu bộ xử lý đưa ra hai hướng dẫn tải riêng biệt, sau đó sử dụng bộ dịch chuyển để ghép các phần đó lại với nhau. Hình phạt điển hình là theo thứ tự tải chậm hơn 3x của mặt hàng đó.
Jerry Coffin

... Nếu bạn nhìn vào mã lắp ráp để đọc int được căn chỉnh và không được sắp xếp, thì việc đọc được căn chỉnh thường là một kiểu ghi nhớ duy nhất. Đọc không được phân bổ có thể dễ dàng 10 dòng lắp ráp khi nó ghép các int lại với nhau, chọn từng byte theo từng byte và đặt tại các vị trí chính xác của thanh ghi.
SF.

2
@ và chậm lại.
Jerry Coffin

8

Các phần tử dữ liệu (ví dụ: thành viên của các lớp và cấu trúc) thường được căn chỉnh trên ranh giới WORD hoặc DWORD cho các bộ xử lý thế hệ hiện tại để cải thiện thời gian truy cập. Việc truy xuất DWORD tại một địa chỉ không chia hết cho 4 yêu cầu ít nhất một chu kỳ CPU bổ sung trên bộ xử lý 32 bit. Vì vậy, nếu bạn có ví dụ ba thành viên char char a, b, c;, họ thực sự có xu hướng chiếm 6 hoặc 12 byte dung lượng lưu trữ.

#pragmacho phép bạn ghi đè lên điều này để đạt được mức sử dụng không gian hiệu quả hơn, với chi phí tốc độ truy cập hoặc tính nhất quán của dữ liệu được lưu trữ giữa các mục tiêu trình biên dịch khác nhau. Tôi đã có rất nhiều niềm vui với việc chuyển đổi từ mã 16 bit sang 32 bit này; Tôi hy vọng việc chuyển sang mã 64 bit sẽ gây ra các loại đau đầu tương tự cho một số mã.


Trên thực tế, char a,b,c;thường sẽ mất 3 hoặc 4 byte dung lượng lưu trữ (ít nhất là trên x86) - đó là vì yêu cầu căn chỉnh của chúng là 1 byte. Nếu không, thì bạn sẽ giải quyết char str[] = "foo";thế nào? Quyền truy cập vào charluôn luôn là một mặt nạ tìm nạp đơn giản, trong khi quyền truy cập vào intcó thể là tìm nạp-hợp nhất hoặc chỉ tìm nạp, tùy thuộc vào việc nó có được căn chỉnh hay không. intcó (trên x86) căn chỉnh 32 bit (4 byte) bởi vì nếu không, bạn sẽ nhận được (nói) một nửa inttrong một DWORDvà một nửa trong cái kia, và điều đó sẽ mất hai lần tra cứu.
Tim Čas

3

Trình biên dịch có thể sắp xếp các thành viên trong các cấu trúc để đạt được hiệu suất tối đa trên nền tảng nhất định. #pragma packchỉ thị cho phép bạn kiểm soát sự liên kết đó. Thông thường bạn nên để nó theo mặc định để có hiệu suất tối ưu. Nếu bạn cần truyền một cấu trúc cho máy từ xa, bạn thường sẽ sử dụng #pragma pack 1để loại trừ mọi căn chỉnh không mong muốn.


2

Một trình biên dịch có thể đặt các thành viên cấu trúc trên các ranh giới byte cụ thể vì lý do hiệu năng trên một kiến ​​trúc cụ thể. Điều này có thể để lại đệm không sử dụng giữa các thành viên. Kết cấu đóng gói buộc các thành viên phải tiếp giáp nhau.

Điều này có thể quan trọng, ví dụ nếu bạn yêu cầu cấu trúc tuân theo một tệp hoặc định dạng truyền thông cụ thể trong đó dữ liệu bạn cần dữ liệu ở các vị trí cụ thể trong một chuỗi. Tuy nhiên, việc sử dụng như vậy không giải quyết được các vấn đề về thời gian cuối, vì vậy mặc dù đã sử dụng nhưng nó có thể không mang theo được.

Nó cũng có thể che phủ chính xác cấu trúc thanh ghi bên trong của một số thiết bị I / O như bộ điều khiển UART hoặc USB chẳng hạn, để đăng ký truy cập phải thông qua một cấu trúc chứ không phải địa chỉ trực tiếp.


1

Bạn chỉ có thể muốn sử dụng điều này nếu bạn đang mã hóa cho một số phần cứng (ví dụ: thiết bị được ánh xạ bộ nhớ) có các yêu cầu nghiêm ngặt để đăng ký thứ tự và căn chỉnh.

Tuy nhiên, điều này trông giống như một công cụ khá cùn để đạt được kết thúc đó. Một cách tiếp cận tốt hơn sẽ là mã hóa trình điều khiển mini trong trình biên dịch chương trình và cung cấp cho nó giao diện gọi C thay vì dò dẫm xung quanh với pragma này.


Tôi thực sự sử dụng nó khá nhiều để tiết kiệm không gian trong các bảng lớn không được truy cập thường xuyên. Ở đó, nó chỉ để tiết kiệm không gian và không cho bất kỳ sự liên kết chặt chẽ. (Chỉ bầu bạn lên, btw. Ai đó đã cho bạn một phiếu bầu tiêu cực.)
Todd Lehman

1

Tôi đã sử dụng nó trong mã trước đây, mặc dù chỉ để giao diện với mã kế thừa. Đây là một ứng dụng Mac OS X Cacao cần tải các tệp ưu tiên từ phiên bản Carbon trước đó (tương thích ngược với phiên bản M68k System 6.5 gốc ... bạn hiểu ý). Các tệp ưu tiên trong phiên bản gốc là kết xuất nhị phân của cấu trúc cấu hình, được sử dụng #pragma pack(1)để tránh chiếm thêm dung lượng và tiết kiệm rác (tức là các byte đệm có thể có trong cấu trúc).

Các tác giả ban đầu của mã cũng đã được sử dụng #pragma pack(1)để lưu trữ các cấu trúc được sử dụng làm thông điệp trong giao tiếp giữa các quá trình. Tôi nghĩ lý do ở đây là để tránh khả năng kích thước đệm không xác định hoặc thay đổi, vì mã đôi khi xem xét một phần cụ thể của cấu trúc thông báo bằng cách đếm một số byte từ đầu (ewww).


1

Tôi đã thấy mọi người sử dụng nó để đảm bảo rằng một cấu trúc lấy toàn bộ dòng bộ đệm để ngăn chia sẻ sai trong bối cảnh đa luồng. Nếu bạn sẽ có một số lượng lớn các đối tượng sẽ được đóng gói lỏng lẻo theo mặc định, nó có thể tiết kiệm bộ nhớ và cải thiện hiệu năng bộ đệm để đóng gói chúng chặt chẽ hơn, mặc dù truy cập bộ nhớ không được phân bổ thường sẽ làm mọi thứ chậm lại.


0

Lưu ý rằng có nhiều cách khác để đạt được tính nhất quán dữ liệu mà gói #pragma cung cấp (ví dụ: một số người sử dụng gói #pragma (1) cho các cấu trúc nên được gửi qua mạng). Ví dụ, xem mã sau đây và đầu ra tiếp theo của nó:

#include <stdio.h>

struct a {
    char one;
    char two[2];
    char eight[8];
    char four[4];
};

struct b { 
    char one;
    short two;
    long int eight;
    int four;
};

int main(int argc, char** argv) {
    struct a twoa[2] = {}; 
    struct b twob[2] = {}; 
    printf("sizeof(struct a): %i, sizeof(struct b): %i\n", sizeof(struct a), sizeof(struct b));
    printf("sizeof(twoa): %i, sizeof(twob): %i\n", sizeof(twoa), sizeof(twob));
}

Đầu ra như sau: sizeof (struct a): 15, sizeof (struct b): 24 sizeof (haia): 30, sizeof (twob): 48

Lưu ý rằng kích thước của struct a chính xác là số byte là bao nhiêu, nhưng struct b có thêm phần đệm (xem phần này để biết chi tiết về phần đệm). Bằng cách làm điều này trái ngược với gói #pragma, bạn có thể kiểm soát việc chuyển đổi "định dạng dây" thành các loại thích hợp. Ví dụ: "char hai [2]" thành "short int" et cetera.


Không, đó là sai. Nếu bạn nhìn vào vị trí trong bộ nhớ của b.two, thì đó không phải là một byte sau b.one (trình biên dịch có thể (và thường sẽ) căn chỉnh b.two để nó phù hợp với truy cập từ). Đối với a.two, nó chính xác là một byte sau a.one. Nếu bạn cần truy cập a.two dưới dạng int ngắn, bạn nên có 2 lựa chọn thay thế, hoặc sử dụng liên kết (nhưng điều này thường thất bại nếu bạn gặp vấn đề về tuổi thọ) hoặc giải nén / chuyển đổi bằng mã (sử dụng hàm ntohX thích hợp)
xryl669

1
sizeoftrả về một size_tcái phải được in ra bằng cách sử dụng%zu . Sử dụng công cụ xác định định dạng sai sẽ gọi hành vi không xác định
phuclv

0

Tại sao một người muốn sử dụng nó?

Để giảm bộ nhớ của cấu trúc

Tại sao người ta không nên sử dụng nó?

  1. Điều này có thể dẫn đến hình phạt hiệu suất, bởi vì một số hệ thống hoạt động tốt hơn trên dữ liệu được căn chỉnh
  2. Một số máy sẽ không đọc được dữ liệu chưa được phân bổ
  3. Mã không di động
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.