Почему 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 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 | +-------+-------+-------+-------+
s
then &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 байтов (2x 32 бита) и 1 байт (2 + 6 бит) соответственно. Вышеприведенная программа (в Linux с использованием gcc) печатает размеры как 4, 8 и 4, где последняя структура дополняется так, чтобы это было одно слово (4 x 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.
Язык Си оставляет компилятору некоторую свободу относительно расположения структурных элементов в памяти:
Язык Си обеспечивает некоторую уверенность программиста в расположении элементов в структуре:
Проблемы, связанные с выравниванием элементов:
Как работает выравнивание:
ps Более подробная информация доступна здесь: "Samuel P.Harbison, Guy L.Steele CA 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: выравнивание важно.