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

568

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

  • 12
    Смотрите этот C FAQ по памяти. c-faq.com/struct/align.esr.html
  • 43
    Анекдот: Был настоящий компьютерный вирус, который поместил свой код в struct paddings в основной программе.
Показать ещё 6 комментариев
Теги:
struct
sizeof
c++-faq

11 ответов

554
Лучший ответ

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

  • Несогласованный доступ может быть жесткой ошибкой (часто SIGBUS).
  • Несогласованный доступ может быть мягкой ошибкой.
    • Либо скорректировано на аппаратном уровне, для скромного снижения производительности.
    • Или исправлено с помощью эмуляции в программном обеспечении, для серьезной деградации производительности.
    • Кроме того, атомарность и другие concurrency -гаранты могут быть разбиты, что приводит к тонким ошибкам.

Здесь приведен пример типичных настроек для процессора 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 и С++ указывают, что выравнивание структуры определяется реализацией. Поэтому каждый компилятор может выбрать выравнивание данных по-разному, что приводит к разным и несовместимым макетам данных. По этой причине при работе с библиотеками, которые будут использоваться разными компиляторами, важно понять, как компиляторы выравнивают данные. Некоторые компиляторы имеют параметры командной строки и/или специальные инструкции #pragma для изменения настроек выравнивания структуры.

  • 37
    Я хочу отметить здесь: большинство процессоров наказывают вас за невыровненный доступ к памяти (как вы упоминали), но вы не можете забыть, что многие полностью запрещают его. В частности, большинство микросхем MIPS будут создавать исключение для неприсоединенного доступа.
  • 34
    Чипы x86 на самом деле довольно уникальны в том смысле, что они допускают невыровненный доступ, хотя и штрафуются; AFAIK большинство фишек будут бросать исключения, а не только некоторые. PowerPC является еще одним распространенным примером.
Показать ещё 12 комментариев
155

Выравнивание пакетов и байтов, как описано в C FAQ здесь:

Это для выравнивания. Многие процессоры не могут получить доступ к 2- и 4-байтам количества (например, ints и long ints), если они переполнены каждый, который-путь.

Предположим, что у вас есть эта структура:

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  |
+-------+-------+-------+-------+
23

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

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

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

Итак, это вопрос выравнивания в основном.

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

  • 5
    «веские причины» Пример: поддержание бинарной совместимости (дополнения) между 32-битной и 64-битной системами для сложной структуры в демонстрационном коде для проверки концепции, который будет продемонстрирован завтра. Иногда необходимость должна иметь приоритет над уместностью.
  • 2
    Все в порядке, кроме случаев, когда вы упоминаете операционную систему. Это проблема скорости процессора, ОС вообще не задействована.
Показать ещё 5 комментариев
11

Это может быть связано с выравниванием и дополнением байт, так что структура выходит на четное количество байтов (или слов) на вашей платформе. Например, в 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» недостаточно для описания вашей платформы. Выравнивание в основном зависит от архитектуры процессора.
  • 0
    - @ Кайл Бертон. Извините, я не понимаю, почему размер структуры "someBits" равен 4, я ожидаю 8 байтов, поскольку объявлено 2 целых числа (2 * sizeof (int)) = 8 байтов. Спасибо
Показать ещё 1 комментарий
9

См. также:

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

6

Размер структуры больше, чем сумма ее частей из-за того, что называется упаковкой. У конкретного процессора есть предпочтительный размер данных, с которым он работает. Большинство современных процессоров предпочитают размер, если 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 байта данных .c находятся в 0x405, 0x406, 0x407, 0x408. На 32-битной машине система считывала data.c в течение одного цикла памяти, но получала бы только 3 из 4 байтов (четвертый байт находится на следующей границе). Таким образом, системе потребуется второй доступ к памяти для получения 4-го байта,

Теперь, если вместо того, чтобы помещать data.c в адрес 0x405, компилятор заполнил структуру на 3 байта и поместил data.c по адресу 0x408, тогда системе понадобится всего 1 цикл для чтения данных, этот элемент данных на 50%. Заполняет эффективность памяти для эффективности обработки. Учитывая, что компьютеры могут иметь огромные объемы памяти (много гигабайт), компиляторы считают, что обмен (скорость по размеру) является разумным.

К сожалению, эта проблема становится убийцей при попытке отправить структуры по сети или даже записать двоичные данные в двоичный файл. Прокладка, вставленная между элементами структуры или класса, может нарушить данные, отправленные в файл или сеть. Чтобы написать переносимый код (тот, который будет использоваться для нескольких разных компиляторов), вам, вероятно, придется обращаться к каждому элементу структуры отдельно, чтобы обеспечить надлежащую "упаковку".

С другой стороны, разные компиляторы имеют разные возможности для управления упаковкой структуры данных. Например, в Visual C/С++ компилятор поддерживает команду #pragma pack. Это позволит вам настроить упаковку и выравнивание данных.

Например:

#pragma pack 1
struct MyStruct
{
    int a;
    char b;
    int c;
    short d;
} myData;

I = sizeof(myData);

Теперь я должен иметь длину 11. Без прагмы я мог бы быть чем угодно от 11 до 14 (а для некоторых систем - целых 32), в зависимости от упаковки по умолчанию компилятора.

  • 0
    Здесь обсуждаются последствия заполнения структуры, но это не отвечает на вопрос.
  • 0
    « ... из-за того, что называется упаковкой ... - я думаю, что вы имеете в виду« заполнение ».« Предпочтительный размер большинства современных процессоров, если 32- битный (4 байта) »- это немного упрощение. Обычно поддерживаются размеры 8, 16, 32 и 64 бита, часто каждый размер имеет свое выравнивание, и я не уверен, что ваш ответ добавляет какую-либо новую информацию, которой еще нет в принятом ответе.
Показать ещё 3 комментария
5

C99 N1256 стандартная черта

http://www.open-std.org/JTC1/SC22/WG14/www/docs/n1256.pdf

6.5.3.4 Оператор sizeof:

3 При применении к операнду, который имеет структуру или тип объединения, результатом является общее количество байтов в таком объекте, включая внутреннюю и заднюю прокладку.

6.7.2.1 Спецификаторы структуры и объединения:

13... Может быть неназванный заполнение внутри объекта структуры, но не в начале.

и

15 В конце структуры или объединения может быть неназванное заполнение.

Новая функция члена C99 гибкого элемента массива (struct S {int is[];};) также может влиять на заполнение:

16 В качестве особого случая последний элемент структуры с более чем одним именованным элементом может имеют неполный тип массива; это называется гибким элементом массива. В большинстве ситуаций, гибкий элемент массива игнорируется. В частности, размер структуры выглядит так, как если бы гибкий элемент массива был исключен, за исключением того, что он может иметь более длинное дополнение, чем упущение будет означать.

Приложение J Проблемы с переносимостью повторяется:

Следующие неуказаны:...

  • Значение байтов заполнения при хранении значений в структурах или объединениях (6.2.6.1)

С++ 11 Стандартная черновая версия N3337

http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2012/n3337.pdf

5.3.3 Размер:

2 При применении к классу, результатом является количество байтов в объекте этого класса, включая любое дополнение, требуемое для размещение объектов этого типа в массиве.

9.2 Члены класса:

Указатель на объект структуры стандартного макета, соответствующим образом преобразованный с использованием reinterpret_cast, указывает на его начальный член (или если этот элемент является битовым полем, а затем блоку, в котором он находится) и наоборот. [ Заметка: Поэтому может быть неназванное заполнение внутри объекта структуры стандартного макета, но не в его начале, по мере необходимости, для обеспечения надлежащего согласования. - конечная нота]

Я знаю только С++, чтобы понять примечание: -)

5

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

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

4

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

  • 8
    Не совсем. В типичных реализациях в структуру добавляется указатель vtable.
3

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

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

Язык C предоставляет некоторую уверенность программисту в компоновке элементов в структуре:

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

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

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

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

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

p.s Более подробная информация доступна здесь: "Сэмюэл П. Харбисон, Гай Л. Стейле C Ссылка, (5.6.2 - 5.6.7)"

2

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

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

Ещё вопросы

Сообщество Overcoder
Наверх
Меню