Чому розмірof для структури не дорівнює сумі розміру кожного члена?


698

Чому sizeofоператор повертає розмір, більший для структури, ніж загальний розмір членів структури?


14
Дивіться цей C поширених запитань про виділення пам'яті. c-faq.com/struct/align.esr.html
Річард Чемберс

48
Анекдот: Існував власне комп’ютерний вірус, який вводив свій код у структурну програмування в хост-програмі.
Елазар

4
@Elazar Це вражає! Я б ніколи не думав, що можна використовувати такі крихітні ділянки для чого-небудь. Чи можете ви надати більше деталей?
ОмарЛ

1
@Wilson - Я впевнений, що він включав багато jmp.
hoodaticus

4
Дивіться структуру прокладки, упаковки : The Lost Art of C
Strucking Packaging

Відповіді:


649

Це відбувається через додавання прокладки, щоб задовольнити обмеження вирівнювання. Вирівнювання структури даних впливає як на продуктивність, так і на коректність програм:

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

Ось приклад використання типових налаштувань для процесора x86 (усі 32-ти та 64-бітні режими):

struct X
{
    short s; /* 2 bytes */
             /* 2 padding bytes */
    int   i; /* 4 bytes */
    char  c; /* 1 byte */
             /* 3 padding bytes */
};

struct Y
{
    int   i; /* 4 bytes */
    char  c; /* 1 byte */
             /* 1 padding byte */
    short s; /* 2 bytes */
};

struct Z
{
    int   i; /* 4 bytes */
    short s; /* 2 bytes */
    char  c; /* 1 byte */
             /* 1 padding byte */
};

const int sizeX = sizeof(struct X); /* = 12 */
const int sizeY = sizeof(struct Y); /* = 8 */
const int sizeZ = sizeof(struct Z); /* = 8 */

Можна мінімізувати розмір структур шляхом сортування членів шляхом вирівнювання (сортування за розміром достатньо для базових типів) (як структура Zу прикладі вище).

ВАЖЛИВО ПРИМІТКА: І стандарти C, і C ++ зазначають, що вирівнювання структури визначено реалізацією. Тому кожен компілятор може вирішити вирівняти дані по-різному, що призведе до різних і несумісних макетів даних. З цієї причини, працюючи з бібліотеками, які будуть використовуватися різними компіляторами, важливо зрозуміти, як компілятори вирівнюють дані. Деякі компілятори мають налаштування командного рядка та / або спеціальні #pragmaоператори для зміни налаштувань вирівнювання структури.


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

35
Чіпи x86 насправді досить унікальні тим, що дозволяють несанкціонований доступ, хоч і штрафований; Більшість фішок AFAIK видасть винятки, а не лише кілька. PowerPC - ще один поширений приклад.
Темний Шикарі

6
Увімкнення прагм для нестандартного доступу, як правило, призводить до того, що ваш код має розмір повітряної кулі на процесорах, які видають помилки нерівності, оскільки код повинен виправити кожну нерівність. ARM також викидає недоліки нерівності.
Майк Діммік

5
@Dark - повністю згоден. Але більшість процесорів настільних ПК - це x86 / x64, тому більшість мікросхем не видають помилок вирівнювання даних;)
Aaron,

27
Неузгоджений доступ до даних, як правило, є функцією, знайденою в архітектурах CISC, і більшість архітектур RISC не включають її (ARM, MIPS, PowerPC, Cell). Насправді більшість мікросхем НЕ є настільними процесорами, оскільки вбудоване правило за кількістю мікросхем і переважна більшість з них є архітектурами RISC.
Лара Дуган

192

Вирівнювання упаковки та байтів, як описано у поширених питаннях C тут :

Це для вирівнювання. Багато процесорів не можуть отримати доступ до 2- і 4-байтових кількостей (наприклад, int і long int), якщо вони забиті в будь-який спосіб.

Припустимо, у вас є така структура:

struct {
    char a[3];
    short int b;
    long int c;
    char d[3];
};

Тепер ви можете подумати, що варто було б упакувати цю структуру в пам’ять так:

+-------+-------+-------+-------+
|           a           |   b   |
+-------+-------+-------+-------+
|   b   |           c           |
+-------+-------+-------+-------+
|   c   |           d           |
+-------+-------+-------+-------+

Але це набагато, набагато простіше на процесорі, якщо компілятор розташовує це так:

+-------+-------+-------+
|           a           |
+-------+-------+-------+
|       b       |
+-------+-------+-------+-------+
|               c               |
+-------+-------+-------+-------+
|           d           |
+-------+-------+-------+

У упакованій версії зауважте, як вам хоча б трохи важко бачити, як обертаються поля b і c? У двох словах, процесору теж важко. Тому більшість компіляторів буде розміщувати структуру (як би з додатковими невидимими полями) таким чином:

+-------+-------+-------+-------+
|           a           | pad1  |
+-------+-------+-------+-------+
|       b       |     pad2      |
+-------+-------+-------+-------+
|               c               |
+-------+-------+-------+-------+
|           d           | pad3  |
+-------+-------+-------+-------+

1
Тепер у чому полягає використання слотів для пам'яті pad1, pad2 та pad3.
Lakshmi Sreekanth Chitla


@EmmEff це може бути неправильно, але я не зовсім розумію це: чому в масиві немає слота пам'яті для вказівника?
Balázs Börcsök

1
@ BalázsBörcsök Це масиви постійного розміру, і тому їх елементи зберігаються безпосередньо в структурі з фіксованими зрушеннями. Компілятор знає все це під час компіляції, тому покажчик неявний. Наприклад, якщо у вас є структура змінної цього типу, яка називається sтоді &s.a == &sі &s.d == &s + 12(з огляду на вирівнювання, показане у відповіді). Вказівник зберігається лише в тому випадку, якщо масиви мають змінний розмір (наприклад, aоголошено char a[]замість char a[3]), але тоді елементи потрібно зберігати десь в іншому місці.
kbolino

27

Якщо ви хочете, наприклад, структура має певний розмір з GCC, наприклад, використовуйте __attribute__((packed)).

У Windows ви можете встановити вирівнювання на один байт при використанні компілятора cl.exe за допомогою параметра / Zp .

Зазвичай ЦП простіше отримати доступ до даних, кратних 4 (або 8), залежно від платформи, а також від компілятора.

Отже, це питання вирівнювання в основному.

Потрібно мати вагомі причини, щоб це змінити.


5
Приклад "вагомих причин" Приклад: Підтримка бінарної сумісності (прокладки) між 32-бітною та 64-бітовою системами для складної структури в демонстраційному коді з підтвердженням концепції, який демонструється завтра. Іноді необхідність повинна мати перевагу над пристойністю.
Mr.Ree

2
Все нормально, крім випадків, коли ви згадуєте Операційну систему. Це проблема швидкості процесора, ОС взагалі не бере участь.
Blaisorblade

3
Ще одна вагома причина - якщо ви завантажуєте потік даних у структуру, наприклад, при аналізі мережевих протоколів.
ceo

1
@dolmen Я лише зазначив, що "Системі Operatin легше отримати доступ до даних" є неправильним, оскільки ОС не має доступу до даних.
Blaisorblade

1
@dolmen Насправді слід говорити про ABI (бінарний інтерфейс програми). Вирівнювання за замовчуванням (використовується, якщо ви не змінюєте його в джерелі) залежить від ABI, і багато ОС підтримують декілька ABI (скажімо, 32- і 64-розрядні, або бінарні файли з різних ОС, або для різних способів складання однакові бінарні файли для однієї ОС). OTOH, яке вирівнювання зручно для продуктивності, залежить від процесора - доступ до пам’яті доступний однаково, якщо ви використовуєте 32 або 64 бітний режим (я не можу коментувати реальний режим, але здається, що навряд чи актуальний для продуктивності в наш час). IIRC Pentium почав віддавати перевагу 8-байтовому вирівнюванню.
Blaisorblade

15

Це може бути пов’язано з вирівнюванням байтів та прокладкою, щоб структура вийшла на рівну кількість байтів (або слів) на вашій платформі. Наприклад, у C на Linux такі 3 структури:

#include "stdio.h"


struct oneInt {
  int x;
};

struct twoInts {
  int x;
  int y;
};

struct someBits {
  int x:2;
  int y:6;
};


int main (int argc, char** argv) {
  printf("oneInt=%zu\n",sizeof(struct oneInt));
  printf("twoInts=%zu\n",sizeof(struct twoInts));
  printf("someBits=%zu\n",sizeof(struct someBits));
  return 0;
}

У членів розміри (у байтах) - 4 байти (32 біти), 8 байт (2х 32 біти) та 1 байт (2 + 6 біт) відповідно. Вищеописана програма (в Linux за допомогою gcc) друкує розміри як 4, 8 та 4 - де остання структура забита, так що це одне слово (4 х 8 бітових байтів на моїй 32-бітовій платформі).

oneInt=4
twoInts=8
someBits=4

4
"C на Linux за допомогою gcc" недостатньо для опису вашої платформи. Вирівнювання здебільшого залежить від архітектури процесора.
долмен

- @ Кайл Бертон. Вибачте, я не розумію, чому розмір структури "someBits" дорівнює 4, я очікую 8 байт, оскільки є 2 заявлені цілі числа (2 * sizeof (int)) = 8 байт. дякую
youpilat13

1
Привіт @ youpilat13, :2і :6фактично вказується 2 та 6 біт, не повних 32-бітових цілих чисел у цьому випадку. someBits.x, будучи лише 2 бітами, може зберігати лише 4 можливі значення: 00, 01, 10 і 11 (1, 2, 3 і 4). Це має сенс? Ось стаття про функцію: geeksforgeeks.org/bit-fields-c
Кайл Бертон,

11

Дивись також:

для Microsoft Visual C:

http://msdn.microsoft.com/en-us/library/2e70t5y1%28v=vs.80%29.aspx

та сумісність заявок GCC з компілятором Microsoft:

http://gcc.gnu.org/onlinedocs/gcc/Structure_002dPacking-Pragmas.html

На додаток до попередніх відповідей, зауважте, що незалежно від упаковки, в C ++ немає гарантій для замовлення членів . Компілятори можуть (і, звичайно, робити) додавати до структури віртуальних вказівників таблиці та членів базових структур. Навіть існування віртуальної таблиці не забезпечується стандартом (реалізація віртуального механізму не визначена), тому можна зробити висновок, що така гарантія просто неможлива.

Я абсолютно впевнений , що член порядку буде гарантовано в C , але я б не розраховував на це, при написанні крос-платформних або крос-компілятор програми.


4
"Я цілком впевнений, що замовлення учасників грунтується на C". Так, C99 говорить: "У структурі об'єкта члени небітових полів та одиниці, в яких перебувають бітові поля, мають адреси, що збільшуються в порядку, в якому вони оголошені". Більш стандарт добрість на: stackoverflow.com/a/37032302/895245
Чіро Сантіллі郝海东冠状病六四事件法轮功


8

Розмір конструкції більший, ніж сума її частин через те, що називається упаковкою. Конкретний процесор має бажаний розмір даних, з яким він працює. Більшість сучасних процесорів бажаного розміру, якщо 32-біт (4 байти). Доступ до пам’яті, коли дані перебувають на такому кордоні, є більш ефективним, ніж те, що обмежує цю межу.

Наприклад. Розглянемо просту структуру:

struct myStruct
{
   int a;
   char b;
   int c;
} data;

Якщо машина є 32-розрядною машиною, а дані вирівнюються по 32-бітовій межі, ми бачимо негайну проблему (якщо не бути вирівнювання структури). У цьому прикладі припустимо, що дані структури починаються з адреси 1024 (0x400 - зауважте, що найнижчі 2 біти дорівнюють нулю, тому дані вирівнюються до 32-бітової межі). Доступ до data.a буде добре працювати, оскільки він починається на межі - 0x400. Доступ до data.b також буде добре працювати, оскільки він знаходиться за адресою 0x404 - ще 32-бітної межею. Але нестандартна структура поставила б data.c за адресою 0x405. 4 байти data.c знаходяться в 0x405, 0x406, 0x407, 0x408. На 32-розрядній машині система читала data.c протягом одного циклу пам'яті, але отримувала б лише 3 з 4-х байт (4-й байт - на наступній межі). Отже, системі доведеться зробити другий доступ до пам'яті, щоб отримати 4-й байт,

Тепер, якщо замість того, щоб поставити data.c за адресою 0x405, компілятор додав структуру на 3 байти і поставив data.c за адресою 0x408, то системі знадобиться лише 1 цикл для зчитування даних, скорочуючи час доступу до цього елемента даних на 50%. Прокладки змінюють ефективність пам'яті для ефективності обробки. Враховуючи, що комп'ютери можуть мати величезну кількість пам'яті (багато гігабайт), компілятори вважають, що своп (швидкість над розміром) є розумним.

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

З іншого боку, різні компілятори мають різні здібності керувати упаковкою структури даних. Наприклад, у Visual C / C ++ компілятор підтримує команду #pragma pack. Це дозволить вам відрегулювати упаковку та вирівнювання даних.

Наприклад:

#pragma pack 1
struct MyStruct
{
    int a;
    char b;
    int c;
    short d;
} myData;

I = sizeof(myData);

Тепер я повинен мати довжину 11. Без прагми я міг би бути від 11 до 14 (а для деяких систем - аж 32), залежно від упаковки компілятора за замовчуванням.


Тут обговорюються наслідки набивання структури, але це не дає відповіді на питання.
Кіт Томпсон

" ... через те, що називається упаковкою. ... - Я думаю, ви маєте на увазі" padding "." Бажаний розмір більшості сучасних процесорів, якщо 32-біт (4 байти) "- Це трохи надмірне спрощення. Зазвичай розміри 8, 16, 32 та 64 біт підтримуються; часто кожен розмір має власне вирівнювання. І я не впевнений, що ваша відповідь додає будь-яку нову інформацію, яка вже не є прийнятою відповіддю.
Кіт Томпсон,

1
Коли я сказав упаковку, я мав на увазі, як компілятор упаковує дані в структуру (і це можна зробити, додаючи дрібні елементи, але це не потрібно для вкладки, але це завжди пакується). Що стосується розміру - я говорив про архітектуру системи, а не про те, що система підтримуватиме доступ до даних (що відрізняється від основної архітектури шини). Щодо вашого остаточного коментаря, я дав спрощене та розширене пояснення одного аспекту компромісу (швидкість проти розміру) - основна проблема програмування. Я також описую спосіб виправити проблему - цього не було у прийнятій відповіді.
sid1138

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

Упаковка - це свого роду перевантажений термін. Це означає, як ви вкладаєте елементи структури в пам'ять. Подібне значення зміщення предметів у коробку (упаковка для переміщення). Це також означає введення елементів у пам'ять без прокладки (на зразок короткої руки для «щільно упакованих»). Потім є командна версія слова в команді #pragma pack.
sid1138

5

Це можна зробити, якщо ви неявно або явно встановили вирівнювання структури. Структура, яка вирівняна 4, завжди буде кратною 4 байтам, навіть якщо розмір її членів буде чимось, що не кратне 4 байтам.

Також бібліотека може бути складена під x86 з 32-бітовими вкладками, і, можливо, порівняння її компонентів у 64-бітовому процесі дало б інший результат, якби ви це робили вручну.


5

C99 N1256 стандартна тяга

http://www.open-std.org/JTC1/SC22/WG14/www/docs/n1256.pdf

6.5.3.4 Розмір оператора :

3 При застосуванні до операнда, який має тип структури або об'єднання, результатом є загальна кількість байтів у такому об'єкті, включаючи внутрішнє та кінцеве прокладки.

6.7.2.1 Структура та об'єднання специфікаторів :

13 ... У структурі об'єкта може бути неназване прокладка, але не на його початку.

і:

15 Можливо, в кінці структури або об'єднання можуть бути неназвані прокладки.

Нова функція гнучких елементів масиву C99 ( struct S {int is[];};) також може вплинути на прокладку:

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

Додаток J Проблеми портативності підтверджує:

Не визначено: ...

  • Значення байтів прокладки при зберіганні значень у структурах чи об'єднаннях (6.2.6.1)

C ++ 11 стандартна тяга N3337

http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2012/n3337.pdf

5.3.3 Розмір :

2 При застосуванні до класу результатом є кількість байтів в об'єкті цього класу, включаючи будь-яку прокладку, необхідну для розміщення об'єктів цього типу в масиві.

9.2 Члени класу :

Вказівник на об'єкт структури структури стандартного макета, відповідним чином перетворений за допомогою reinterpret_cast, вказує на його початковий член (або якщо цей член є бітовим полем, потім на одиницю, в якій він знаходиться) і навпаки. [Примітка: Тому може бути неназвана накладка в об'єкті структури структури стандартного макета, але не на його початку, як це необхідно для досягнення відповідного вирівнювання. - кінцева примітка]

Я знаю лише достатньо C ++, щоб зрозуміти замітку :-)


4

На додаток до інших відповідей, структура може (але зазвичай не має) віртуальних функцій, і в цьому випадку розмір структури також буде містити простір для vtbl.


8
Не зовсім. У типових реалізаціях, що додається в структуру, це покажчик vtable .
Дон Уейкфілд

3

Мова C залишає компілятору деяку свободу щодо розташування структурних елементів у пам'яті:

  • Дірки в пам'яті можуть з’являтися між будь-якими двома компонентами і після останнього компонента. Це було пов'язано з тим, що певні типи об'єктів на цільовому комп'ютері можуть бути обмежені межами адресації
  • розмір "отворів пам'яті", включений в результат оператора sizeof. Розмір тільки не включає розмір гнучкого масиву, який доступний у C / C ++
  • Деякі реалізації мови дозволяють керувати компонуванням структур пам'яті за допомогою параметрів прагми та компілятора

Мова C надає певну впевненість програмісту в компонуванні елементів у структурі:

  • компілятори, необхідні для призначення послідовності компонентів, що збільшують адреси пам'яті
  • Адреса першого компонента збігається з початковою адресою структури
  • неназвані бітові поля можуть бути включені в структуру до потрібних вирівнювань адреси сусідніх елементів

Проблеми, пов'язані з вирівнюванням елементів:

  • Різні комп'ютери по-різному вирівнюють краї об’єктів
  • Різні обмеження по ширині бітового поля
  • Комп'ютери відрізняються тим, як зберігати байти одним словом (Intel 80x86 та Motorola 68000)

Як працює вирівнювання:

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

ps Більш детальна інформація доступна тут: "Samuel P.Harbison, Guy L.Steele CA CA, (5.6.2 - 5.6.7)"


2

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

struct pixel {
    unsigned char red;   // 0
    unsigned char green; // 1
    unsigned int alpha;  // 4 (gotta skip to an aligned offset)
    unsigned char blue;  // 8 (then skip 9 10 11)
};

// next offset: 12

Архітектура x86 завжди могла отримати нестандартні адреси. Однак це відбувається повільніше, і коли перекошування перекриває дві різні лінії кешу, то воно вилучає дві лінії кешу, коли вирівняний доступ вилучає лише одну.

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

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

TL; DR: важливим є вирівнювання.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.