Структура набивки и упаковки
Рассматривать:
struct mystruct_A
{
char a;
int b;
char c;
} x;
struct mystruct_B
{
int b;
char a;
} y;
Размеры конструкций 12 и 8 соответственно.
Эти структуры дополнены или упакованы?
Когда происходит заполнение или упаковка?
11 ответов
Отступ выравнивает элементы структуры по "естественным" адресным границам - скажем, int
члены будут иметь смещения, которые mod(4) == 0
на 32-битной платформе. Заполнение включено по умолчанию. Он вставляет следующие "пробелы" в вашу первую структуру:
struct mystruct_A {
char a;
char gap_0[3]; /* inserted by compiler: for alignment of b */
int b;
char c;
char gap_1[3]; /* -"-: for alignment of the whole struct in an array */
} x;
Упаковка, с другой стороны, мешает компилятору выполнять заполнение - это должно быть явно запрошено - в GCC это __attribute__((__packed__))
, так что следующее:
struct __attribute__((__packed__)) mystruct_A {
char a;
int b;
char c;
};
будет производить структуру размера 6
на 32-битной архитектуре.
Однако обратите внимание: доступ к невыровненной памяти медленнее на архитектурах, которые позволяют это (например, x86 и amd64), и явно запрещен на архитектурах со строгим выравниванием, таких как SPARC.
(Приведенные выше ответы объяснили причину довольно ясно, но, кажется, не совсем ясно о размере заполнения, поэтому я добавлю ответ в соответствии с тем, что я узнал из Lost Art of C Structure Packing)
Выравнивание памяти (для структуры)
Правила:
- Перед каждым отдельным элементом будет добавление, чтобы оно начиналось с адреса, кратного его размеру.
например, в 64-битной системе,int
должен начинаться с адреса, кратного 4, иlong
к 8,short
на 2. char
а такжеchar[]
являются специальными, может быть любым адресом памяти, поэтому им не требуется заполнение перед ними.- За
struct
кроме необходимости выравнивания для каждого отдельного элемента, размер всей структуры будет выровнен по размеру, кратному размеру наибольшего отдельного элемента, путем заполнения в конце.
например, если самый большой член структурыlong
затем делится на 8,int
затем на 4,short
затем на 2.
Порядок участника:
- Порядок членов может повлиять на фактический размер структуры, так что имейте это в виду. например,
stu_c
а такжеstu_d
из приведенного ниже примера имеют одинаковые элементы, но в другом порядке, и приводят к разному размеру для двух структур.
Адрес в памяти (для структуры)
Правила:
- 64-битная система
Структурный адрес начинается с(n * 16)
байт. (Вы можете видеть в примере ниже, все напечатанные шестнадцатеричные адреса структур заканчиваются0
,)
Причина: возможный самый большой отдельный элемент структуры составляет 16 байтов (long double
).
Пустое место:
- Пустое пространство между 2 структурами может использоваться неструктурными переменными, которые могут вписываться.
например, вtest_struct_address()
ниже переменнаяx
находится между смежной структуройg
а такжеh
,
Неважно,x
объявлен,h
адрес не изменится,x
просто использовал пустое пространство, котороеg
потрачены впустую.
Подобный случай дляy
,
пример
(для 64-битной системы)
memory_align.c:
/**
* Memory align & padding - for struct.
* compile: gcc memory_align.c
* execute: ./a.out
*/
#include <stdio.h>
// size is 8, 4 + 1, then round to multiple of 4 (int's size),
struct stu_a {
int i;
char c;
};
// size is 16, 8 + 1, then round to multiple of 8 (long's size),
struct stu_b {
long l;
char c;
};
// size is 24, l need padding by 4 before it, then round to multiple of 8 (long's size),
struct stu_c {
int i;
long l;
char c;
};
// size is 16, 8 + 4 + 1, then round to multiple of 8 (long's size),
struct stu_d {
long l;
int i;
char c;
};
// size is 16, 8 + 4 + 1, then round to multiple of 8 (double's size),
struct stu_e {
double d;
int i;
char c;
};
// size is 24, d need align to 8, then round to multiple of 8 (double's size),
struct stu_f {
int i;
double d;
char c;
};
// size is 4,
struct stu_g {
int i;
};
// size is 8,
struct stu_h {
long l;
};
// test - padding within a single struct,
int test_struct_padding() {
printf("%s: %ld\n", "stu_a", sizeof(struct stu_a));
printf("%s: %ld\n", "stu_b", sizeof(struct stu_b));
printf("%s: %ld\n", "stu_c", sizeof(struct stu_c));
printf("%s: %ld\n", "stu_d", sizeof(struct stu_d));
printf("%s: %ld\n", "stu_e", sizeof(struct stu_e));
printf("%s: %ld\n", "stu_f", sizeof(struct stu_f));
printf("%s: %ld\n", "stu_g", sizeof(struct stu_g));
printf("%s: %ld\n", "stu_h", sizeof(struct stu_h));
return 0;
}
// test - address of struct,
int test_struct_address() {
printf("%s: %ld\n", "stu_g", sizeof(struct stu_g));
printf("%s: %ld\n", "stu_h", sizeof(struct stu_h));
printf("%s: %ld\n", "stu_f", sizeof(struct stu_f));
struct stu_g g;
struct stu_h h;
struct stu_f f1;
struct stu_f f2;
int x = 1;
long y = 1;
printf("address of %s: %p\n", "g", &g);
printf("address of %s: %p\n", "h", &h);
printf("address of %s: %p\n", "f1", &f1);
printf("address of %s: %p\n", "f2", &f2);
printf("address of %s: %p\n", "x", &x);
printf("address of %s: %p\n", "y", &y);
// g is only 4 bytes itself, but distance to next struct is 16 bytes(on 64 bit system) or 8 bytes(on 32 bit system),
printf("space between %s and %s: %ld\n", "g", "h", (long)(&h) - (long)(&g));
// h is only 8 bytes itself, but distance to next struct is 16 bytes(on 64 bit system) or 8 bytes(on 32 bit system),
printf("space between %s and %s: %ld\n", "h", "f1", (long)(&f1) - (long)(&h));
// f1 is only 24 bytes itself, but distance to next struct is 32 bytes(on 64 bit system) or 24 bytes(on 32 bit system),
printf("space between %s and %s: %ld\n", "f1", "f2", (long)(&f2) - (long)(&f1));
// x is not a struct, and it reuse those empty space between struts, which exists due to padding, e.g between g & h,
printf("space between %s and %s: %ld\n", "x", "f2", (long)(&x) - (long)(&f2));
printf("space between %s and %s: %ld\n", "g", "x", (long)(&x) - (long)(&g));
// y is not a struct, and it reuse those empty space between struts, which exists due to padding, e.g between h & f1,
printf("space between %s and %s: %ld\n", "x", "y", (long)(&y) - (long)(&x));
printf("space between %s and %s: %ld\n", "g", "y", (long)(&y) - (long)(&h));
return 0;
}
int main(int argc, char * argv[]) {
test_struct_padding();
// test_struct_address();
return 0;
}
Результат выполнения - test_struct_padding()
:
stu_a: 8
stu_b: 16
stu_c: 24
stu_d: 16
stu_e: 16
stu_f: 24
stu_g: 4
stu_h: 8
Результат выполнения - test_struct_address()
:
stu_g: 4
stu_h: 8
stu_f: 24
address of g: 0x7fffd63a95d0 // struct variable - address dividable by 16,
address of h: 0x7fffd63a95e0 // struct variable - address dividable by 16,
address of f1: 0x7fffd63a95f0 // struct variable - address dividable by 16,
address of f2: 0x7fffd63a9610 // struct variable - address dividable by 16,
address of x: 0x7fffd63a95dc // non-struct variable - resides within the empty space between struct variable g & h.
address of y: 0x7fffd63a95e8 // non-struct variable - resides within the empty space between struct variable h & f1.
space between g and h: 16
space between h and f1: 16
space between f1 and f2: 32
space between x and f2: -52
space between g and x: 12
space between x and y: 12
space between g and y: 8
Я знаю, что этот вопрос старый, и большинство ответов здесь очень хорошо объясняет заполнение, но, пытаясь понять его сам, я подумал, что помогло "визуальное" представление о происходящем.
Процессор считывает память "кусками" определенного размера (слова). Скажем, процессорное слово имеет длину 8 байтов. Он будет смотреть на память как большой ряд 8-байтовых строительных блоков. Каждый раз, когда ему нужно получить некоторую информацию из памяти, он достигнет одного из этих блоков и получит его.
Как показано на рисунке выше, не имеет значения, где находится символ (длиной 1 байт), поскольку он будет находиться внутри одного из этих блоков, что потребует от процессора обработки только 1 слова.
Когда мы имеем дело с данными размером более одного байта, такими как 4-байтовое int или 8-байтовое двойное число, то, как они выровнены в памяти, влияет на то, сколько слов придется обрабатывать центральным процессором. Если 4-байтовые блоки выровнены таким образом, что они всегда соответствуют внутренней части блока (адрес памяти кратен 4), то нужно обработать только одно слово. В противном случае часть из 4 байтов может иметь часть себя в одном блоке и часть в другом, требуя, чтобы процессор обработал 2 слова для чтения этих данных.
То же самое относится к 8-байтовому двойному символу, за исключением того, что теперь он должен быть в адресе памяти, кратном 8, чтобы гарантировать, что он всегда будет внутри блока.
Это касается 8-байтового текстового процессора, но концепция применима к другим размерам слов.
Заполнение работает, заполняя промежутки между этими данными, чтобы удостовериться, что они выровнены с этими блоками, таким образом улучшая производительность, читая память.
Однако, как указано в ответах других, иногда пространство имеет большее значение, чем сама производительность. Возможно, вы обрабатываете много данных на компьютере, на котором недостаточно ОЗУ (можно использовать пространство подкачки, но оно НАМНОГО медленнее). Вы можете расположить переменные в программе до тех пор, пока не будет выполнено наименьшее заполнение (как это было продемонстрировано в некоторых других ответах), но если этого недостаточно, вы можете явно отключить заполнение, что и является упаковкой.
Упаковка конструкции подавляет заполнение конструкции, прокладку, используемую, когда выравнивание имеет наибольшее значение, упаковку, используемую, когда пространство имеет наибольшее значение.
Некоторые компиляторы предоставляют #pragma
подавить заполнение или сделать его упакованным в n байтов. Некоторые предоставляют ключевые слова для этого. Обычно прагма, которая используется для изменения заполнения структуры, будет иметь следующий формат (зависит от компилятора):
#pragma pack(n)
Например, ARM предоставляет __packed
ключевое слово, чтобы подавить заполнение структуры. Просмотрите руководство по компилятору, чтобы узнать больше об этом.
Таким образом, упакованная структура - это структура без заполнения.
Обычно будут использоваться упакованные конструкции
сэкономить место
отформатировать структуру данных для передачи по сети, используя некоторый протокол (конечно, это не очень хорошая практика, потому что вам нужно
разобраться с порядком байтов)
Обивка и упаковка - это только два аспекта одного и того же:
- упаковка или выравнивание - это размер, до которого округляется каждый элемент
- отступ - это дополнительное пространство, добавляемое для выравнивания
В mystruct_A
при условии, что выравнивание по умолчанию равно 4, каждый элемент выровнен по кратному 4 байтам. Поскольку размер char
1, отступ для a
а также c
4 - 1 = 3 байта, в то время как для заполнения не требуется int b
а это уже 4 байта. Это работает так же для mystruct_B
,
Переменные хранятся по любым адресам, кратным его выравниванию (обычно по размеру). Итак, заполнение / упаковка не только для структуры. Собственно, у всех данных есть собственное требование выравнивания :
int main(void) {
// We assume the `c` is stored as first byte of machine word
// as a convenience! If the `c` was stored as a last byte of previous
// word, there is no need to pad bytes before variable `i`
// because `i` is automatically aligned in a new word.
char c; // starts from any addresses divisible by 1(any addresses).
char pad[3]; // not-used memory for `i` to start from its address.
int32_t i; // starts from any addresses divisible by 4.
Это похоже на struct, но с некоторыми отличиями. Во-первых, мы можем сказать, что существует два вида заполнения: а) Чтобы каждый член правильно начинал со своего адреса, некоторые байты вставляются между членами. б) Чтобы правильно запустить следующий экземпляр структуры с ее адреса, к каждой структуре добавляются несколько байтов:
// Example for rule 1 below.
struct st {
char c; // starts from any addresses divisible by 4, not 1.
char pad[3]; // not-used memory for `i` to start from its address.
int32_t i; // starts from any addresses divisible by 4.
};
// Example for rule 2 below.
struct st {
int32_t i; // starts from any addresses divisible by 4.
char c; // starts from any addresses.
char pad[3]; // not-used memory for next `st`(or anything that has same
// alignment requirement) to start from its own address.
};
- Первый член структуры всегда начинается с любых адресов, делящихся на собственное требование выравнивания структуры, которое определяется требованием выравнивания наибольшего члена (здесь
4
, выравниваниеint32_t
). С обычными переменными дело обстоит иначе. Обычные переменные могут начинать любые адреса, кратные их выравниванию, но это не относится к первому члену структуры. Как вы знаете, адрес структуры совпадает с адресом ее первого члена. - Внутри структуры могут быть дополнительные завершающие байты с заполнением, в результате чего следующая структура (или следующий элемент в массиве структур) начинается с ее собственного адреса. Думать о
struct st arr[2];
. Делать (arr[1]
первого члена), начиная с адреса, кратного 4, мы должны добавить 3 байта в конец каждой структуры.
Это то, что я узнал из книги «Утерянное искусство упаковки конструкций» .
ПРИМЕЧАНИЕ. Вы можете исследовать требования к выравниванию типа данных.
_Alignof
оператор. Кроме того, вы можете получить смещение члена внутри структуры через
offsetof
макрос.
Правила заполнения:
- Каждый член структуры должен иметь адрес, кратный его размеру. Заполнение вставляется между элементами или в конце структуры, чтобы гарантировать соблюдение этого правила. Это сделано для более простого и эффективного доступа к шине для оборудования.
- Отступ в конце структуры определяется на основе размера самого большого члена структуры.
Почему Правило 2: рассмотрите следующую структуру,
Если бы мы создали массив (из 2 структур) этой структуры, в конце не потребовалось бы заполнения:
Следовательно, размер структуры = 8 байтов
Предположим, мы должны были создать другую структуру, как показано ниже:
Если бы мы создавали массив этой структуры, у нас было бы 2 варианта количества байтов заполнения, необходимых в конце.
A. Если мы добавим 3 байта в конце и выровняем его для int, а не Long:
Б. Если мы добавим 7 байтов в конце и выровняем его по Long:
Начальный адрес второго массива кратен 8(т. Е. 24). Размер структуры = 24 байта
Следовательно, выравнивая начальный адрес следующего массива структуры с кратным наибольшему члену (т. Е. Если бы мы должны были создать массив этой структуры, первый адрес второго массива должен начинаться с адреса, который является кратным самого большого члена структуры. Вот он, 24(3 * 8)), мы можем вычислить количество байтов заполнения, необходимых в конце.
Эти конструкции набиты или набиты?
Они мягкие.
Единственная возможность, которая изначально приходит на ум, где они могут быть упакованы, - это если бы они были одинакового размера, так что минимальный размер
char/int/char
структура не допускает заполнения, то же самое для
int/char
состав.
Однако для этого потребуются оба
sizeof(int)
и быть четырьмя (чтобы получить двенадцать и восемь размеров). Вся теория разваливается, поскольку стандарт гарантирует, что
sizeof(char)
это всегда один.
Мы
char
а также
int
одинаковой ширины, размеры будут один и один, а не четыре и четыре. Итак, чтобы затем получить размер двенадцать, после последнего поля должно быть заполнение.
Когда происходит набивка или упаковка?
Когда этого требует реализация компилятора. Компиляторы могут вставлять отступы между полями и после последнего поля (но не перед первым полем).
Обычно это делается для повышения производительности, поскольку некоторые типы работают лучше, когда они выровнены по определенным границам. Есть даже некоторые архитектуры, которые откажутся работать (например, откажутся), если вы попытаетесь получить доступ к невыровненным данным (да, я смотрю на вас, ARM).
Обычно вы можете управлять упаковкой / заполнением (что на самом деле является противоположным концом одного и того же спектра) с помощью специфичных для реализации функций, таких как
#pragma pack
. Даже если вы не можете сделать это в своей конкретной реализации, вы можете проверить свой код во время компиляции, чтобы убедиться, что он соответствует вашим требованиям (с использованием стандартных функций C, а не материалов, специфичных для конкретной реализации).
Например:
// C11 or better ...
#include <assert.h>
struct strA { char a; int b; char c; } x;
struct strB { int b; char a; } y;
static_assert(sizeof(struct strA) == sizeof(char)*2 + sizeof(int), "No padding allowed");
static_assert(sizeof(struct strB) == sizeof(char) + sizeof(int), "No padding allowed");
Что-то вроде этого откажется компилироваться, если в этих структурах есть какие-либо отступы.
Там нет ничего об этом! Кто хочет понять предмет, должен сделать следующее,
- Прочитайте Потерянное Искусство Упаковки Структуры, написанное Эриком С. Рэймондом
- Посмотрите на пример кода Эрика
- И последнее, но не менее важное: не забывайте следующее правило о заполнении, согласно которому структура выравнивается в соответствии с требованиями выравнивания самого большого типа.
Упаковка структуры выполняется только тогда, когда вы явно указываете компилятору упаковать структуру. Обивка - это то, что вы видите. Ваша 32-битная система дополняет каждое поле выравниванием слов. Если бы вы сказали своему компилятору упаковать структуры, они бы составляли 6 и 5 байтов соответственно. Не делай этого, хотя. Он не переносим и заставляет компиляторы генерировать гораздо более медленный (а иногда даже ошибочный) код.
Выравнивание структуры данных - это способ упорядочения и доступа к данным в памяти компьютера. Он состоит из двух отдельных, но связанных вопросов: выравнивание данных и заполнение структуры данных. Когда современный компьютер выполняет чтение или запись по адресу памяти, он выполняет это в виде кусочков размером в слово (например, 4-байтных кусков в 32-разрядной системе) или больше. Выравнивание данных означает размещение данных по адресу памяти, равному некоторому кратному размеру слова, что повышает производительность системы благодаря тому, как процессор обрабатывает память. Чтобы выровнять данные, может быть необходимо вставить несколько бессмысленных байтов между концом последней структуры данных и началом следующей, которая является заполнением структуры данных.
- Чтобы выровнять данные в памяти, один или несколько пустых байтов (адресов) вставляются (или остаются пустыми) между адресами памяти, которые выделяются для других элементов структуры во время выделения памяти. Эта концепция называется структурным заполнением.
- Архитектура компьютерного процессора такова, что он может считывать из памяти 1 слово (4 байта в 32-разрядном процессоре) за раз.
- Чтобы использовать это преимущество процессора, данные всегда выровнены как 4-байтовый пакет, что приводит к вставке пустых адресов между адресами других членов.
- Из-за этой концепции дополнения структуры в C размер структуры всегда не совпадает с тем, что мы думаем.