Почему sizeof для структуры не равен сумме sizeof каждого члена?

698

Почему sizeofоператор возвращает размер, больший для структуры, чем общий размер элементов структуры?

Kevin
источник
14
Смотрите этот C FAQ по памяти. c-faq.com/struct/align.esr.html
Ричард Чамберс,
48
Анекдот: Был настоящий компьютерный вирус, который поместил свой код в struct paddings в основной программе.
Элазар
4
@Elazar Это впечатляет! Я никогда бы не подумал, что можно использовать такие крошечные области для чего-либо. Можете ли вы предоставить более подробную информацию?
OmarL
1
@ Уилсон - я уверен, что это связано с большим количеством JMP.
hoodaticus
4
См. Структурное заполнение, упаковка : Утраченное структурное упаковочное искусство С Эрик Рэймонд
EsmaeelE

Ответы:

650

Это связано с добавлением отступов для удовлетворения ограничений выравнивания. Согласование структуры данных влияет как на производительность, так и на правильность программ:

  • Неправильный доступ может быть серьезной ошибкой (часто 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
@ Темно - полностью согласен. Но большинство настольных процессоров - x86 / x64, поэтому большинство чипов не выдают ошибок выравнивания данных;)
Аарон
27
Нераспределенный доступ к данным, как правило, есть в архитектурах CISC, и большинство архитектур RISC не включают его (ARM, MIPS, PowerPC, Cell). На самом деле, большинство микросхем НЕ являются настольными процессорами, для встроенного правила по количеству микросхем, и подавляющее большинство из них - архитектуры RISC.
Лара Дуган
193

Упаковка и выравнивание байтов, как описано в 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  |
+-------+-------+-------+-------+
EmmEff
источник
1
Теперь, что такое использование слотов памяти pad1, pad2 и pad3.
Лакшми Срикант Читла
@EmmEff это может быть неправильно, но я не совсем понимаю: почему в массивах нет слота памяти для указателя?
Balázs Börcsök
1
@ BalázsBörcsök Это массивы постоянного размера, поэтому их элементы хранятся непосредственно в структуре с фиксированными смещениями. Компилятор знает все это во время компиляции, поэтому указатель неявный. Например, если у вас есть переменная структуры этого типа, называемая sthen &s.a == &sи &s.d == &s + 12(с учетом выравнивания, показанного в ответе). Указатель сохраняется только в том случае, если массивы имеют переменный размер (например, aбыл объявленchar a[] вместо char a[3]), но тогда элементы должны храниться где-то еще.
Кболино
28

Если вы хотите, чтобы структура имела определенный размер с GCC, например, используйте __attribute__((packed)) .

В Windows вы можете установить выравнивание на один байт при использовании компилятора cl.exe с параметром / Zp .

Обычно процессору проще получить доступ к данным, кратным 4 (или 8), в зависимости от платформы, а также от компилятора.

Так что это вопрос выравнивания в принципе.

У вас должны быть веские причины, чтобы изменить это.

INS
источник
5
«веские причины» Пример: поддержание согласованности двоичной совместимости (дополнения) между 32-разрядными и 64-разрядными системами для сложной структуры в демонстрационном коде для проверки концепции, который будет продемонстрирован завтра. Иногда необходимость должна иметь приоритет над уместностью.
Mr.Ree
2
Все в порядке, кроме случаев, когда вы упоминаете операционную систему. Это проблема скорости процессора, ОС вообще не задействована.
Blaisorblade
3
Еще одна веская причина, если вы вставляете поток данных в структуру, например, при разборе сетевых протоколов.
Генеральный директор
1
@dolmen Я только что указал, что «Системе Operatin легче получать доступ к данным» неверно, поскольку ОС не имеет доступа к данным.
Blaisorblade
1
@dolmen На самом деле нужно говорить о ABI (двоичный интерфейс приложения). Выравнивание по умолчанию (используется, если вы не меняете его в источнике) зависит от ABI, и многие ОС поддерживают несколько ABI (скажем, 32- и 64-разрядных, или для двоичных файлов из разных ОС, или для разных способов компиляции одни и те же двоичные файлы для той же ОС). OTOH, то, какое выравнивание удобно с точки зрения производительности, зависит от процессора - к памяти обращаются одинаково, независимо от того, используете ли вы 32- или 64-битный режим (я не могу комментировать реальный режим, но в настоящее время вряд ли имеет значение для производительности). IIRC Pentium начал предпочитать 8-байтовое выравнивание.
Blaisorblade
16

Это может быть связано с выравниванием байтов и заполнением, так что структура выходит на четное количество байтов (или слов) на вашей платформе. Например в 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
Кайл Бертон
источник
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
Кайл Бертон
12

Смотрите также:

для 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 , но я бы не рассчитывал на это, при написании кросс-платформенных или кросс-компилятор программы.

lkanab
источник
4
«Я совершенно уверен, что членский приказ хрюкает в C». Да, C99 говорит: «Внутри структурного объекта члены, не являющиеся битовыми полями, и блоки, в которых находятся битовые поля, имеют адреса, которые увеличиваются в порядке их объявления». Больше стандартного совершенства по адресу: stackoverflow.com/a/37032302/895245
Сиро Сантилли 郝海东 冠状 病 六四 事件 法轮功
9

Размер структуры больше, чем сумма ее частей из-за того, что называется упаковкой. Определенный процессор имеет предпочтительный размер данных, с которым он работает. Предпочтительный размер большинства современных процессоров - 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), в зависимости от упаковки компилятора по умолчанию.

sid1138
источник
Здесь обсуждаются последствия заполнения структуры, но это не отвечает на вопрос.
Кит Томпсон
« ... из-за того, что называется упаковкой ... - я думаю, что вы имеете в виду« заполнение ».« Предпочтительный размер большинства современных процессоров, если 32- битный (4 байта) »- это немного упрощение. Обычно поддерживаются размеры 8, 16, 32 и 64 бита, часто каждый размер имеет свое выравнивание, и я не уверен, что ваш ответ добавляет какую-либо новую информацию, которой еще нет в принятом ответе
Кит Томпсон,
1
Когда я сказал «упаковка», я имел в виду, как компилятор упаковывает данные в структуру (и это можно сделать, заполнив небольшие элементы, но не нужно заполнять, но он всегда упаковывает). Что касается размера - я говорил об архитектуре системы, а не о том, что система будет поддерживать для доступа к данным (что сильно отличается от базовой архитектуры шины). Что касается вашего последнего комментария, я дал упрощенное и расширенное объяснение одного из аспектов компромисса (скорость по сравнению с размером) - основная проблема программирования. Я также описываю способ решения проблемы - этого не было в принятом ответе.
sid1138
«Упаковка» в этом контексте обычно относится к распределению элементов более плотно, чем по умолчанию, как с #pragma pack. Если члены располагаются по их выравниванию по умолчанию, я бы сказал, что структура не упакована.
Кит Томпсон,
Упаковка - это перегруженный термин. Это означает, как вы помещаете элементы структуры в память. Аналогично смыслу помещения предметов в коробку (упаковка для перемещения). Это также означает помещение элементов в память без заполнения (что-то вроде короткой руки для «плотно упакованного»). Затем в команде #pragma pack есть версия слова команды.
sid1138
6

Это можно сделать, если вы явно или неявно установили выравнивание структуры. Структура с выравниванием 4 всегда будет кратна 4 байтам, даже если размер ее членов будет не кратным 4 байтам.

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

Орион Адриан
источник
6

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 ++, чтобы понять примечание :-)

Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功
источник
4

В дополнение к другим ответам, структура может (но обычно не имеет) иметь виртуальные функции, и в этом случае размер структуры будет также включать пространство для vtbl.

JohnMcG
источник
8
Не совсем. В типичных реализациях в структуру добавляется указатель vtable .
Дон Уэйкфилд
4

Язык Си оставляет компилятору некоторую свободу относительно расположения структурных элементов в памяти:

  • дыры в памяти могут появляться между любыми двумя компонентами и после последнего компонента. Это было связано с тем, что определенные типы объектов на целевом компьютере могут быть ограничены границами адресации
  • Размер «дыр в памяти» включен в результат оператора sizeof. Размер только не включает размер гибкого массива, который доступен в C / C ++
  • Некоторые реализации языка позволяют контролировать структуру структур памяти с помощью параметров прагмы и компилятора.

Язык Си обеспечивает некоторую уверенность программиста в расположении элементов в структуре:

  • компиляторам необходимо назначить последовательность компонентов, увеличивающих адреса памяти
  • Адрес первого компонента совпадает с начальным адресом структуры
  • безымянные битовые поля могут быть включены в структуру для требуемого выравнивания адресов соседних элементов

Проблемы, связанные с выравниванием элементов:

  • Различные компьютеры выравнивают края объектов по-разному
  • Различные ограничения на ширину битового поля
  • Компьютеры отличаются тем, как хранить байты в слове (Intel 80x86 и Motorola 68000)

Как работает выравнивание:

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

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

bruziuz
источник
3

Идея состоит в том, что из соображений скорости и кэша операнды должны читаться с адресов, выровненных по их естественному размеру. Чтобы это произошло, компилятор дополняет элементы структуры так, чтобы следующий элемент или следующая структура были выровнены.

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: выравнивание важно.

DigitalRoss
источник