Почему sizeof для структуры не равен сумме sizeof каждого члена?
Почему sizeof
оператор возвращает размер, больший для структуры, чем общий размер элементов структуры?
13 ответов
Это связано с добавлением отступов для удовлетворения ограничений выравнивания. Согласование структуры данных влияет как на производительность, так и на правильность программ:
- Неправильный доступ может быть серьезной ошибкой (часто
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 FAQ здесь:
Это для выравнивания. Многие процессоры не могут получить доступ к 2- и 4-байтовым количествам (например, целым и длинным целым), если они забиты всевозможными способами.
Предположим, у вас есть эта структура:
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 | +-------+-------+-------+-------+
Если вы хотите, чтобы структура имела определенный размер с 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 байтов (2x 32 бита) и 1 байт (2+6 бит) соответственно. Вышеприведенная программа (в Linux с использованием gcc) печатает размеры как 4, 8 и 4, где последняя структура дополняется так, чтобы это было одно слово (4 x 8 битных байтов на моей 32-битной платформе).
oneInt=4
twoInts=8
someBits=4
Смотрите также:
для 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), в зависимости от упаковки компилятора по умолчанию.
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 всегда будет кратна 4 байтам, даже если размер ее членов будет не кратным 4 байтам.
Кроме того, библиотека может быть скомпилирована под x86 с 32-разрядными целочисленными значениями, и вы, возможно, сравниваете ее компоненты в 64-разрядном процессе, что даст вам другой результат, если вы будете делать это вручную.
Язык Си оставляет компилятору некоторую свободу относительно расположения структурных элементов в памяти:
- дыры в памяти могут появляться между любыми двумя компонентами и после последнего компонента. Это было связано с тем, что определенные типы объектов на целевом компьютере могут быть ограничены границами адресации
- Размер "дыр в памяти" включен в результат оператора sizeof. Размер только не включает размер гибкого массива, который доступен в C/C++
- Некоторые реализации языка позволяют контролировать структуру структур памяти с помощью параметров прагмы и компилятора.
Язык Си обеспечивает некоторую уверенность программиста в расположении элементов в структуре:
- компиляторы должны назначить последовательность компонентов, увеличивающих адреса памяти
- Адрес первого компонента совпадает с начальным адресом структуры
- безымянные битовые поля могут быть включены в структуру для требуемого выравнивания адресов смежных элементов
Проблемы, связанные с выравниванием элементов:
- Разные компьютеры по-разному выравнивают края объектов
- Различные ограничения на ширину битового поля
- Компьютеры отличаются тем, как хранить байты в слове (Intel 80x86 и Motorola 68000)
Как работает выравнивание:
- Объем, занимаемый структурой, рассчитывается как размер выровненного отдельного элемента массива таких структур. Структура должна заканчиваться так, чтобы первый элемент следующей следующей структуры не нарушал требования выравнивания
ps Более подробная информация доступна здесь: "Samuel P.Harbison, Guy L.Steele C A Reference, (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: выравнивание важно.
В дополнение к другим ответам, структура может (но обычно не имеет) иметь виртуальные функции, и в этом случае размер структуры также будет включать пространство для vtbl.
Среди других хорошо объясненных ответов о выравнивании памяти и заполнении / упаковке структуры есть кое-что, что я обнаружил в самом вопросе, внимательно прочитав его.
"Почему нет
sizeof
для структуры, равной суммеsizeof
каждого члена?""Почему
sizeof
оператор возвращает размер структуры больше, чем общий размер членов структуры"?
Оба вопроса предполагают нечто совершенно неправильное. По крайней мере, в общем, не ориентированном на примеры представлении, как здесь.
Результат sizeof
операнд, примененный к объекту структуры, может быть равен суммеsizeof
применяется к каждому члену отдельно. Он не обязательно должен быть больше / отличаться.
Если нет причин для заполнения, память не будет заполнена.
Одна из наиболее реализаций, если структура содержит только члены одного типа:
struct foo {
int a;
int b;
int c;
} bar;
Предполагая sizeof(int) == 4
, размер конструкции bar
будет равняться сумме размеров всех участников вместе, sizeof(bar) == 12
. Здесь нет отступов.
То же самое, например, здесь:
struct foo {
short int a;
short int b;
int c;
} bar;
Предполагая sizeof(short int) == 2
а также sizeof(int) == 4
. Сумма выделенных байтов дляa
а также b
равно выделенным байтам для c
, самый большой член, и с ним все идеально совмещено. Таким образом,sizeof(bar) == 8
.
Это также объект второго по популярности вопроса о заполнении структур, здесь:
учитывая много информации (объяснение) выше.
И я просто хотел бы поделиться некоторым методом, чтобы решить эту проблему.
Вы можете избежать этого, добавив pragma pack
#pragma pack(push, 1)
// your structure
#pragma pack(pop)