#прагма ефект упаковки


233

Мені було цікаво, чи може хтось пояснити мені, що #pragma packробить оператор препроцесора, і що ще важливіше, чому хтось хоче його використовувати?

Я перевірив сторінку MSDN , яка запропонувала деяке розуміння, але я сподівався почути більше від людей із досвідом. Я бачив це в коді раніше, хоча, здається, більше не можу знайти.


1
Це примушує певне вирівнювання / упаковка структури, але, як і всі #pragmaдирективи, вони визначені для реалізації.
dreamlax

A mod s = 0де А - адреса, а 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
Можливо, варто додати і недоліки упаковки. (Доступ до об'єктів у
нескінченному

11
Здається, згаданий узгоджений "покарання за ефективність" може бути корисним для деяких систем danluu.com/3c-conflict .

6
@Pacerier Не дуже. Цей пост говорить про деяке досить екстремальне вирівнювання (вирівнювання за межами 4 КБ). Центральний процесор очікує певних мінімальних вирівнювань для різних типів даних, але в гіршому випадку потрібно 8-байтне вирівнювання (не враховуючи типи векторів, які можуть вимагати вирівнювання 16 або 32 байтів). Якщо не вирівнювати ці межі, зазвичай ви отримуєте помітне враження від продуктивності (оскільки завантаження може бути виконано як дві операції замість однієї), але тип або добре вирівняний, або його немає. Більш чітке вирівнювання, ніж це, нічого не купує (і руйнує використання кешу
затримка

6
Іншими словами, подвійний розраховує бути на 8-байтовій межі. Якщо розмістити його на 7-байтній межі, це зашкодить продуктивності. Але розміщення його на 16, 32, 64 або 4096 байтовій межі не купує вас нічого вище того, що 8-байтова межа вже дала вам. Ви отримаєте таку ж ефективність від центрального процесора, при цьому набагато гірше буде використовувати кеш із причин, викладених у цій публікації.
jalf

4
Отже, урок - це не "упаковка вигідна" (упаковка порушує природне вирівнювання типів, що шкодить продуктивності), а просто "не
перестарайтеся за рамками

27

#pragmaвикористовується для надсилання не портативних (як у цьому компіляторі) повідомлень компілятору. Такі речі, як відключення певних попереджень та пакувальних конструкцій, є загальними причинами. Вимкнення конкретних попереджень особливо корисно, якщо ви компілюєте з попередженнями, коли увімкнено прапор помилок.

#pragma packконкретно використовується для вказівки, що упакована структура не повинна мати своїх членів. Це корисно, коли у вас є інтерфейс, відображений на пам'яті, на апаратне забезпечення, і вам потрібно мати можливість точно контролювати, куди вказують різні члени структури. Це, зокрема, не є хорошою оптимізацією швидкості, оскільки більшість машин набагато швидше справляється з узгодженими даними.


17
Щоб скасувати після цього, зробіть це: #pragma pack (push, 1) та #pragma pack (pop)
маль.

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 рядків складання, оскільки воно з'єднує цілий ряд, збираючи його за байтом і розміщуючи в правильних місцях регістра.
СФ.

2
@SF. Це може бути - але навіть коли це не так, не вводьте в оману - на процесорі x86 (для одного очевидного прикладу) операції виконуються апаратно, але ви все одно отримуєте приблизно однаковий набір операцій і уповільнення.
Джеррі Труну

8

Елементи даних (наприклад, члени класів і структур), як правило, вирівнюються за межами WORD або DWORD для процесорів поточного покоління з метою покращення часу доступу. Для отримання DWORD за адресою, не розділеною на 4, потрібно щонайменше один додатковий цикл процесора на 32-бітовому процесорі. Отже, якщо у вас є, наприклад, три члени char a, b, c;, вони фактично мають 6 або 12 байт.

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


Насправді, char a,b,c;зазвичай буде потрібно 3 або 4 байти (хоча б на x86) - це тому, що вимога їх вирівнювання дорівнює 1 байту. Якби цього не було, то як би ти поводився char str[] = "foo";? Доступ до a charзавжди є простою маскою "shift-shift", тоді як доступ до en intможе бути результатом злиття або просто вилученням, залежно від того, вирівняний він чи ні. intмає (на x86) 32-бітове (4 байтове) вирівнювання, оскільки в іншому випадку ви отримаєте (скажімо) половину intв одному DWORDі половину в іншому, і для цього знадобиться два пошуку.
Тім Час

3

Компілятор може вирівняти членів у структурах для досягнення максимальної продуктивності на певній платформі. #pragma packДиректива дозволяє контролювати це вирівнювання. Зазвичай ви повинні залишити його за замовчуванням для досягнення оптимальної продуктивності. Якщо вам потрібно передати структуру віддаленій машині, ви зазвичай використовуєте #pragma pack 1для виключення небажаного вирівнювання.


2

Компілятор може розміщувати членів структури на конкретних межах байтів з міркувань продуктивності для певної архітектури. Це може залишити невикористані прокладки між членами. Упаковка структури змушує членів бути суміжними.

Це може бути важливо, наприклад, якщо вам потрібна структура, щоб відповідати певному файлу або формату зв'язку, де потрібні вам дані, щоб вони знаходилися на певних позиціях в межах послідовності. Однак таке використання не стосується проблем, пов'язаних з ендіасності, тому, хоч і використовується, воно може бути не портативним.

Він може також точно накладати внутрішню структуру регістру деяких пристроїв вводу / виводу, таких як UART або USB-контролер, наприклад, для того, щоб доступ до регістру здійснювався через структуру, а не прямі адреси.


1

Ви, ймовірно, хочете використовувати це лише в тому випадку, якщо ви кодували якесь обладнання (наприклад, пристрій, зіставлене з пам’яттю), в якому були жорсткі вимоги до впорядкування та вирівнювання реєстру.

Однак це виглядає як досить тупий інструмент для досягнення цієї мети. Кращим підходом було б кодування міні-драйвера в асемблері та надання йому інтерфейсу для виклику С, а не спіткання з цією прагмою.


Я фактично використовую його досить багато, щоб заощадити місце у великих таблицях, до яких не часто звертаються. Там потрібно лише заощадити простір, а не будь-яке суворе вирівнювання. (Тільки що проголосували за вас, btw. Хтось дав вам негативний голос.)
Todd Lehman

1

Раніше я використовував його в коді, хоча тільки для інтерфейсу зі застарілим кодом. Це була програма Mac OS X Cocoa, якій потрібно було завантажити файли переваг з більш ранньої версії Carbon (яка сама була зворотно сумісною з оригінальною версією системи M68k System 6.5 ... ви розумієте). Файли уподобань в оригінальній версії були двійковим дампами конфігураційної структури, які використовували #pragma pack(1)для уникнення зайвого місця та економії мотлоху (тобто байтів, що в іншому випадку будуть у структурі).

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


1

Я бачив, як люди використовують його, щоб переконатися, що структура займає цілий рядок кешу, щоб запобігти обміну помилками у багатопотоковому контексті. Якщо у вас буде велика кількість об'єктів, які за замовчуванням будуть розфасовані, це може зберегти пам'ять та покращити продуктивність кешу, щоб упакувати їх більш щільно, хоча нестандартний доступ до пам'яті зазвичай сповільнює роботу, тому може виникнути зворотний бік.


0

Зауважте, що існують інші способи досягнення послідовності даних, які пропонує #pragma pack (наприклад, деякі користувачі використовують #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

Зверніть увагу на те, як розмір struct a є саме таким, яким є кількість байтів, але в структурі b додано доповнення (див. Це для деталей прокладки). Роблячи це, на відміну від пакету #pragma, ви можете контролювати перетворення "дротяного формату" у відповідні типи. Наприклад, "char two [2]" в "short int" et cetera.


Ні, це неправильно Якщо ви подивитеся на позицію в пам'яті b.two, це не один байт після b.one (компілятор може (і часто буде) вирівнювати b.two, щоб він вирівнювався до доступу до слова). Для a.two, це рівно один байт після a.one. Якщо вам потрібно отримати доступ до 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.