Pull to refresh

Несколько слов о размере структур в С/С++ и о том, почему так получилось

Reading time 3 min
Views 32K
Ниже по тексту термином «платформа» будем называть любой заданный набор из процессора, компилятора и операционной системы, под которой скомпилированный код будет запускаться.

Исторически язык C создавался таким, что среди главных целей, положенных в его основу есть такие:
  • быть максимально независимым от какой-то конкретной платформы,
  • быть максимально эффективным на всех платформах. В идеале — на всех.

Немножко о разнообразии платформ. Их (платформ) существует огромное количество — среди процессоров есть и 16-битные, и 32-битные, и 64-битные. Есть такие, которые умеют выполнять операции с плавающей точкой на аппаратном уровне, какие-то поддерживают операции с двойной точностью, а в каких-то процессорах FPU отсутствует полностью. Процессоры отличаются также внутренним порядком следования байт в слове (big/little endian), как именно процессор работает с внешней памятью, и т.д. и т.п.

И на весь этот зоопарк существует один-единственный Стандарт языка C. Как же это удалось? Вот тут и начинается самое интересное.


Язык C даёт очень мало ограничений на то, какими именно должны быть его базовые типы (char, short, int, long, float, double). Задаётся минимальное количество бит для некоторых типов (например, тип «char» должен быть минимум 8 бит, «int»/«short» — 16, «long» — 32), задаётся, что размеры базовых типов кратны размеру char'а, а сам размер char'а тождественно равен единице.

И, для примера: у программ под MS-DOS размер int'а и short'а совпадал, а сами эти типы были 16-битными. Для платформы Win32 размер int'а уже стал другим — 32 бита.

Немного экзотики: есть платформы (от Texas Instruments, к примеру), где размер базовых типов сильно отличается от «привычного». Например, размер всех базовых типов одинаков и равен единице. Т.е. sizeof(int)==sizeof(char)==sizeof(double). Да, именно так — и char, и short на этой платформе — 32-битные. Стандарт при этом не нарушается. Или такая платформа: тип «long» занимает в памяти 8 байт, но реально используется там только 5 Т.е. sizeof(long)==8, а ULONG_MAX=1099511627775 или 240-1. Тоже это всё не противоречит Стандарту, но поначалу удивляет.

Другой важный момент из «мира железа», который обязан был учитываться при разработке Стандарта — это то, как процессор работает с памятью. Если не углубляться в то, как функционирует шина адреса, шина данных и микросхемы памяти, то для подавляющего числа архитектур существует следующее правило: если читаем/записываем одной командой N-байтовую величину, то её адрес обязан быть кратным N. То есть, если в память записывается 4-байтовый int, адрес должен делиться нацело на 4. Аналогично для 2-байтовых short'ов и т.п.

Что происходит, если это правило не выполняется? На разных платформах — по-разному: на некоторых (ARM'ы, например) происходит прерывание работы процессора и управление передаётся в ядро ОС, на других (DSP от TI) процессор просто молча запишет по ближайшему кратному адресу (т.е. не туда, куда сказали, а рядом), а на платформе x86 процессор (для некоторых типов данных) сделает так, как подразумевал программист, за счёт некоторого падения производительности. Важно тут понять одно — все процессоры работают одинаково, когда адрес является выравненным, и кто во что горазд — при невыполнении этого требования. Именно поэтому определение выравнивания (alignment) определяется в самом начале Стандарта и постоянно упоминается при описании модели памяти языка.

К чему же это всё приводит с точки зрения программиста? Лучше всего это показать на примере. Допустим, у нас есть такая структура:
struct Foo {
int iiii;
char c;
};


С точки зрения программиста (неопытного) размер этой структуры равен sizeof(int)+sizeof(char)=4+1=5 (подразумеваем, что размер int'а — 4 байта). Однако, что произойдёт, если объявить массив из нескольких элементов такого типа? В памяти они будут располагаться так:
image

Или словами: поле iiii первого элемента располагается по выравненному адресу, а для второго элемента это уже не выполняется. Т.е. при размере структуры Foo равном 5 нельзя выполнить требования модели памяти С.

Для того же, чтобы и овцы были целы, и волки сыты, компилятор вставляет «невидимые» (для программиста) дополнительных 3 байта в конец структуры (так называемые padding bytes). Это приводит к тому, что размер структуры становится равен 8 и в памяти массив начинает располагаться так:
image

Однако, это ещё не всё, что делает с этой структурой компилятор. Кроме того, что размер Foo равен 8, компилятор также запоминает, что минимальное требование по выравниванию для всей этой структуры — 4 байта. Отличия легко показать на следующем примере:
struct Foo {
int iiii;
char c;
};

struct Bar {
char c8[8];
};

struct Test1 {
char c;
Foo foo;
};

struct Test2 {
char c;
Bar bar;
};

Здесь интересны такие моменты: размер и Foo и Bar — одинаковый и равен 8-ми байтам. Но требования по выравниваю у Foo — 4 байта, а у Bar — 1. Это приводит к тому, что в структуре Test1, между 'c' и 'foo' компилятор вставляет дополнительных 3 байта, чтобы гарантировать, что поле 'foo' всегда будет начинаться по адресу, кратному 4-м. В структуре же Test2 ничего такого делать не надо, в результате — sizeof(Test1) равен 12, а sizeof(Test2) — 9. То есть получили разный результат, комбинируя «кирпичики» одного и того же размера!

Если это всё интересно, то тему можно продолжить.
Tags:
Hubs:
+89
Comments 53
Comments Comments 53

Articles