Чому sizeof
оператор повертає розмір, більший для структури, ніж загальний розмір членів структури?
Чому sizeof
оператор повертає розмір, більший для структури, ніж загальний розмір членів структури?
Відповіді:
Це відбувається через додавання прокладки, щоб задовольнити обмеження вирівнювання. Вирівнювання структури даних впливає як на продуктивність, так і на коректність програм:
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
оператори для зміни налаштувань вирівнювання структури.
Вирівнювання упаковки та байтів, як описано у поширених питаннях 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 | +-------+-------+-------+-------+
s
тоді &s.a == &s
і &s.d == &s + 12
(з огляду на вирівнювання, показане у відповіді). Вказівник зберігається лише в тому випадку, якщо масиви мають змінний розмір (наприклад, a
оголошено char a[]
замість char a[3]
), але тоді елементи потрібно зберігати десь в іншому місці.
Якщо ви хочете, наприклад, структура має певний розмір з GCC, наприклад, використовуйте __attribute__((packed))
.
У Windows ви можете встановити вирівнювання на один байт при використанні компілятора cl.exe за допомогою параметра / Zp .
Зазвичай ЦП простіше отримати доступ до даних, кратних 4 (або 8), залежно від платформи, а також від компілятора.
Отже, це питання вирівнювання в основному.
Потрібно мати вагомі причини, щоб це змінити.
Це може бути пов’язано з вирівнюванням байтів та прокладкою, щоб структура вийшла на рівну кількість байтів (або слів) на вашій платформі. Наприклад, у 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
:2
і :6
фактично вказується 2 та 6 біт, не повних 32-бітових цілих чисел у цьому випадку. someBits.x, будучи лише 2 бітами, може зберігати лише 4 можливі значення: 00, 01, 10 і 11 (1, 2, 3 і 4). Це має сенс? Ось стаття про функцію: geeksforgeeks.org/bit-fields-c
Дивись також:
для 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 , але я б не розраховував на це, при написанні крос-платформних або крос-компілятор програми.
Розмір конструкції більший, ніж сума її частин через те, що називається упаковкою. Конкретний процесор має бажаний розмір даних, з яким він працює. Більшість сучасних процесорів бажаного розміру, якщо 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), залежно від упаковки компілятора за замовчуванням.
#pragma pack
. Якщо учасники розподіляються за типовим вирівнюванням, я б загалом сказав, що структура не упакована.
Це можна зробити, якщо ви неявно або явно встановили вирівнювання структури. Структура, яка вирівняна 4, завжди буде кратною 4 байтам, навіть якщо розмір її членів буде чимось, що не кратне 4 байтам.
Також бібліотека може бути складена під x86 з 32-бітовими вкладками, і, можливо, порівняння її компонентів у 64-бітовому процесі дало б інший результат, якби ви це робили вручну.
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 ++, щоб зрозуміти замітку :-)
На додаток до інших відповідей, структура може (але зазвичай не має) віртуальних функцій, і в цьому випадку розмір структури також буде містити простір для vtbl.
Мова C залишає компілятору деяку свободу щодо розташування структурних елементів у пам'яті:
Мова C надає певну впевненість програмісту в компонуванні елементів у структурі:
Проблеми, пов'язані з вирівнюванням елементів:
Як працює вирівнювання:
ps Більш детальна інформація доступна тут: "Samuel P.Harbison, Guy L.Steele CA CA, (5.6.2 - 5.6.7)"
Ідея полягає в тому, що з огляду на швидкість і кеш-пам'ять операнди повинні зчитуватися з адрес, вирівняних за їх природним розміром. Щоб цього не сталося, компілятор прокладає структури структури, так що наступний член або наступна структура будуть вирівняні.
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: важливим є вирівнювання.