#pragma pack effect


233

Мне было интересно, может ли кто-нибудь объяснить мне, что #pragma packделает оператор препроцессора, и, что более важно, почему он захочет его использовать.

Я проверил страницу MSDN , которая дала некоторое представление, но я надеялся услышать больше от людей с опытом. Я видел это в коде раньше, хотя я не могу найти, где больше.


1
Это вызывает определенное выравнивание / упаковку структуры, но, как и все #pragmaдирективы, они определены реализацией.
Dreamlax

A mod s = 0где A - адрес, а s - размер типа данных; это проверяет, не смещены ли данные.
legends2k

Ответы:


422

#pragma packуказывает компилятору упаковать элементы структуры с определенным выравниванием. Большинство компиляторов, когда вы объявляете структуру, вставляют заполнение между членами, чтобы гарантировать, что они выровнены по соответствующим адресам в памяти (обычно кратным размеру типа). Это позволяет избежать снижения производительности (или явной ошибки) на некоторых архитектурах, связанных с доступом к переменным, которые не выровнены должным образом. Например, даны 4-байтовые целые и следующая структура:

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

Компилятор может разместить структуру в памяти следующим образом:

|   1   |   2   |   3   |   4   |  

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

и sizeof(Test)будет 4 × 3 = 12, даже если он содержит только 6 байтов данных. Наиболее распространенный вариант использования #pragma(насколько мне известно) - при работе с аппаратными устройствами, когда вам нужно убедиться, что компилятор не вставляет заполнение в данные, а каждый элемент следует предыдущему. С #pragma pack(1), структура выше будет выглядеть так:

|   1   |

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

И sizeof(Test)будет 1 × 6 = 6.

С #pragma pack(2), структура выше будет выглядеть так:

|   1   |   2   | 

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

И sizeof(Test)будет 2 × 4 = 8.

Порядок переменных в структуре также важен. С переменными, упорядоченными следующим образом:

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

и с #pragma pack(2), структура будет выглядеть так:

|   1   |   2   | 

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

и sizeOf(Test)будет 3 × 2 = 6.


76
Возможно, стоит добавить недостатки упаковки. (доступ к невыровненным объектам в лучшем случае медленный , но на некоторых платформах это приведет к ошибкам.)
jalf

11
Кажется, что упомянутые выравнивания « снижение производительности» могут быть полезны для некоторых систем danluu.com/3c-conflict .

6
@Pacerier Не совсем. Этот пост говорит о некотором довольно экстремальном выравнивании (выравнивание по границам 4 КБ). Процессор ожидает определенные минимальные выравнивания для различных типов данных, но для них требуется, в худшем случае, 8-байтовое выравнивание (не считая векторных типов, которые могут потребовать 16 или 32-байтовое выравнивание). Отсутствие выравнивания по этим границам обычно дает заметное снижение производительности (поскольку нагрузку, возможно, придется выполнять как две операции вместо одной), но тип либо выровнен, либо нет. Более строгое выравнивание, чем это, ничего не покупает (и разрушает использование кэша
jalf

6
Другими словами, ожидается, что двойник находится на 8-байтовой границе. Помещение его в 7-байтовую границу ухудшит производительность. Но если поместить его на границу в 16, 32, 64 или 4096 байтов, вы не получите ничего сверх того, что вам уже дала 8-байтовая граница. Вы получите ту же производительность от ЦП, но при этом использование кеша будет намного хуже по причинам, изложенным в этом посте.
Половина

4
Таким образом, урок состоит не в том, что «упаковка полезна» (упаковка нарушает естественное выравнивание типов, что ухудшает производительность), а просто в том, что «не выровнять сверх того, что требуется»
января

27

#pragmaиспользуется для отправки непереносимых (как только в этом компиляторе) сообщений компилятору. Такие вещи, как отключение определенных предупреждений и структура упаковки, являются общими причинами. Отключение определенных предупреждений особенно полезно, если вы компилируете предупреждения, когда включен флаг ошибок.

#pragma packв частности, используется для указания того, что упаковываемая структура не должна выравнивать свои элементы. Это полезно, когда у вас есть интерфейс, отображаемый в памяти для части аппаратного обеспечения, и вам нужно точно контролировать, куда указывают различные члены структуры. Это особенно не хорошая оптимизация скорости, так как большинство машин гораздо быстрее справляются с согласованными данными.


17
Чтобы отменить потом, сделайте это: #pragma pack (push, 1) и #pragma pack (pop)
malhal

16

Он сообщает компилятору границы для выравнивания объектов в структуре. Например, если у меня есть что-то вроде:

struct foo { 
    char a;
    int b;
};

На типичном 32-разрядном компьютере вы обычно «хотели бы» иметь 3 байта заполнения aи bтаким образом, чтобы bон попадал на 4-байтовую границу, чтобы максимизировать скорость доступа (и это обычно происходит по умолчанию).

Однако, если вам нужно соответствовать внешне определенной структуре, вы хотите убедиться, что компилятор разметит вашу структуру точно в соответствии с этим внешним определением. В этом случае вы можете дать компилятору #pragma pack(1)указание не вставлять какие-либо отступы между членами - если определение структуры включает заполнение между членами, вы вставляете его явно (например, обычно с именованными элементами unusedNили ignoreN, или что-то в этом роде). порядок).


«вы обычно« хотели бы »иметь 3 байта заполнения между a и b, чтобы b попадал на 4-байтовую границу, чтобы максимизировать скорость доступа» - как 3 байта заполнения увеличивают скорость доступа?
Эшвин

8
@Ashwin: размещение bна 4-байтовой границе означает, что процессор может загрузить его, выдав одну 4-байтовую загрузку. Хотя это в некоторой степени зависит от процессора, но если он находится на нечетной границе, есть большая вероятность, что загрузка потребует от процессора выдачи двух отдельных инструкций загрузки, а затем используйте переключатель, чтобы собрать эти части вместе. Типичный штраф составляет порядка 3-кратной медленной загрузки этого предмета.
Джерри Гроб

... если вы посмотрите на ассемблерный код для чтения выровненных и не выровненных int, выровненное чтение обычно является одной мнемоникой. Чтение без выравнивания может легко составлять 10 строк сборки, поскольку оно объединяет целое, отбирая его побайтно и размещая в правильных местах регистра.
SF.

2
@SF .: Это может быть - но даже если это не так, не вводите в заблуждение - на процессоре x86 (для одного очевидного примера) операции выполняются аппаратно, но вы все равно получаете примерно тот же набор операций и замедление.
Джерри Гроб

8

Элементы данных (например, члены классов и структур) обычно выровнены по границам WORD или DWORD для процессоров текущего поколения, чтобы сократить время доступа. Для извлечения DWORD по адресу, который не делится на 4, требуется как минимум один дополнительный цикл ЦП на 32-разрядном процессоре. Так, если у вас есть, например, три члена char char a, b, c;, они на самом деле имеют тенденцию занимать 6 или 12 байт памяти.

#pragmaпозволяет вам переопределить это для достижения более эффективного использования пространства за счет скорости доступа или для согласованности хранимых данных между различными целями компилятора. Мне было очень весело с этим переходом с 16-битного на 32-битный код; Я ожидаю, что портирование на 64-битный код вызовет такие же головные боли для некоторого кода.


Фактически, char a,b,c;обычно занимает 3 или 4 байта памяти (по крайней мере, на x86), потому что их требование выравнивания составляет 1 байт. Если бы не было, как бы вы справились char str[] = "foo";? Доступ к a charвсегда является простой маской fetch-shift-mask, а доступ к a intможет быть fetch-fetch-merge или просто fetch, в зависимости от того, выровнен он или нет. intимеет (на x86) 32-битное (4 байта) выравнивание, потому что в противном случае вы получите (скажем) половину от intодного DWORDдо половины от другого, и для этого потребуется два поиска.
Тим Час

3

Компилятор может выравнивать элементы в структурах для достижения максимальной производительности на определенной платформе. #pragma packДиректива позволяет контролировать это выравнивание. Обычно вы должны оставить его по умолчанию для оптимальной производительности. Если вам нужно передать структуру на удаленную машину, вы обычно будете использовать ее #pragma pack 1для исключения нежелательного выравнивания.


2

Компилятор может размещать элементы структуры на определенных границах байтов из соображений производительности на конкретной архитектуре. Это может оставить неиспользованные отступы между участниками. Упаковка структуры заставляет участников быть смежными.

Это может быть важно, например, если вам требуется, чтобы структура соответствовала определенному файлу или формату связи, где данные, которые вам нужны, должны располагаться в определенных позициях в последовательности. Однако такое использование не решает проблемы с порядком байтов, поэтому, хотя оно и используется, оно не может быть переносимым.

Он также может точно перекрывать структуру внутреннего регистра некоторого устройства ввода-вывода, такого как, например, контроллер UART или USB, чтобы доступ к регистру осуществлялся через структуру, а не через прямые адреса.


2

Я видел, как люди используют его, чтобы убедиться, что структура занимает целую строку кэша, чтобы предотвратить ложное совместное использование в многопоточном контексте. Если у вас будет большое количество объектов, которые по умолчанию будут свободно упакованы, это может сэкономить память и повысить производительность кэша, чтобы упаковать их, хотя доступ к памяти без выравнивания, как правило, замедляет работу, что может привести к недостаткам.


1

Скорее всего, вы захотите использовать это, только если кодируете какое-то оборудование (например, устройство с отображенной памятью), которое имеет строгие требования к упорядочению и выравниванию регистров.

Тем не менее, это выглядит довольно тупым инструментом для достижения этой цели. Лучшим подходом было бы написать мини-драйвер на ассемблере и дать ему интерфейс вызова C, а не возиться с этой прагмой.


На самом деле я использую его довольно часто, чтобы сэкономить место в больших таблицах, к которым не часто обращаются. Там только для экономии места, а не для строгого выравнивания. (Просто проголосовал за вас, кстати. Кто-то проголосовал за вас.)
Тодд Леман

1

Я использовал его в коде раньше, но только для взаимодействия с устаревшим кодом. Это было приложение Mac OS X Cocoa, которое должно было загружать файлы предпочтений из более ранней версии Carbon (которая была обратно совместима с исходной версией M68k System 6.5 ... вы понимаете). Файлы настроек в исходной версии представляли собой двоичный дамп структуры конфигурации, в котором использовался метод, #pragma pack(1)чтобы не занимать дополнительное пространство и не экономить ненужные файлы (т. Е. Байты заполнения, которые в противном случае были бы в структуре).

Первоначальные авторы кода также использовали #pragma pack(1)для хранения структур, которые использовались в качестве сообщений в межпроцессном взаимодействии. Я думаю, что причина здесь состояла в том, чтобы избежать возможности неизвестных или измененных размеров заполнения, поскольку код иногда просматривал определенную часть структуры сообщения, подсчитывая количество байтов с самого начала (ewww).


0

Обратите внимание, что есть и другие способы обеспечения согласованности данных, которые предлагает пакет #pragma (например, некоторые люди используют #pragma pack (1) для структур, которые должны передаваться по сети). Например, посмотрите следующий код и его последующий вывод:

#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));
}

Вывод выглядит следующим образом: sizeof (struct a): 15, sizeof (struct b): 24 sizeof (twoa): 30, sizeof (twob): 48

Обратите внимание , как размер структуру а именно то , что счетчик байт, но структура б имеет отступы добавили (см это подробно о кожухах). Делая это в отличие от пакета #pragma, вы можете контролировать преобразование «проводного формата» в соответствующие типы. Например, "char two [2]" в "short int" и так далее.


Нет, это не так. Если вы посмотрите на позицию в памяти b.two, это не один байт после b.one (компилятор может (и будет часто) выравнивать b.two, чтобы он соответствовал доступу к словам). Для двух это ровно один байт после одного. Если вам нужно получить доступ к a.two как короткое int, у вас должно быть 2 варианта: либо использовать объединение (но обычно это не получается, если у вас есть проблема с порядком байтов), либо распаковать / преобразовать с помощью кода (используя соответствующую функцию ntohX)
xryl669

1
sizeofвозвращает, size_tкоторый должен быть распечатан с использованием%zu . Использование неправильного спецификатора формата вызывает неопределенное поведение
phuclv

0

Почему один хочет использовать это?

Уменьшить память о структуре

Почему не стоит использовать это?

  1. Это может привести к снижению производительности, поскольку некоторые системы лучше работают с согласованными данными.
  2. Некоторые машины не смогут прочитать данные без выравнивания
  3. Код не переносимый
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.