Странное распределение памяти на С++

Я создал простой класс Storer на С++, играя с распределением памяти. Он содержит шесть переменных поля, все из которых назначаются в конструкторе:

int x;
int y;
int z;
char c;
long l;
double d;

Мне было интересно, как хранятся эти переменные, поэтому я написал следующий код:

Storer *s=new Storer(5,4,3,'a',5280,1.5465);
cout<<(long)s<<endl<<endl;
cout<<(long)&(s->x)<<endl;
cout<<(long)&(s->y)<<endl;
cout<<(long)&(s->z)<<endl;
cout<<(long)&(s->c)<<endl;
cout<<(long)&(s->l)<<endl;
cout<<(long)&(s->d)<<endl;

Меня очень интересовал выход:

33386512

33386512
33386516
33386520
33386524
33386528
33386536

Почему char c занимает четыре байта? sizeof (char) возвращает, конечно, 1, так почему же программа выделяет больше памяти, чем нужно? Это подтверждается тем, что слишком много памяти выделяется следующим кодом:

cout<<sizeof(s->c)<<endl;
cout<<sizeof(Storer)<<endl;
cout<<sizeof(int)+sizeof(int)+sizeof(int)+sizeof(char)+sizeof(long)+sizeof(double)<<endl;

который печатает:

1
32
29

подтверждающий, что, действительно, 3 байта выделяются бесполезно. Может ли кто-нибудь объяснить мне, почему это происходит? Спасибо.

Ответ 1

Выравнивание данных и добавление компилятора hi!

ЦП не имеет понятия типа, то, что он получает в своем 32-битном (или 64-битном, или 128-битном (SSE), или 256-битном (AVX) - пусть он просто прост в 32) требует регистров чтобы быть правильно выровненными для правильной и эффективной обработки. Представьте простой сценарий, в котором у вас есть char, за которым следует int. В 32-битной архитектуре это 1 байт для char и 4 байта для целого числа.

32-разрядный регистр должен был бы сломаться по его границе, только взяв 3 байта целого числа и оставив четвертый байт для "второго прогона". Он не может обрабатывать данные должным образом таким образом, поэтому компилятор добавит отступы, чтобы убедиться, что все материалы обработаны эффективно. А это означает добавление определенного количества отступов в зависимости от типа, о котором идет речь.

Почему возникает проблема несоосности?

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

Не могли бы вы его визуализировать?

Визуально - первый зеленый байт - это упомянутый char, а три зеленых байта плюс первый красный из второго блока - это 4-байтовый int, colorcoded на 4-байтовой границе доступа (мы говорим о 32-битном регистре). "Вместо этого часть" внизу показывает идеальную установку, в которой int попадает в регистр должным образом (char получает прописью в послушание где-то с изображения):

enter image description here

Узнайте больше о выравнивании данных, что очень удобно, когда вы имеете дело с причудливыми расширениями набора команд, таких как SSE (128-битные регистры) или AVX (256-битные регистры), поэтому необходимо соблюдать особую осторожность, чтобы оптимизация векторизации не разыгрывается (выравнивание по 16-байтовой границе для SSE, 16 * 8 → 128 бит).

Дополнительные замечания по пользовательскому выравниванию

phonetagger сделал правильную точку в комментариях, что есть директивы pragma, которые могут быть назначены через препроцессор для принудительного компилятора, чтобы выровнять данные так, как это задает пользователь, программист. Но такие директивы, как #pragma pack(...), являются выражением компилятору, что вы знаете, что делаете и что лучше всего для вас. Убедитесь, что вы это сделали, потому что, если вы не в состоянии разместить свою среду, вы можете столкнуться с различными штрафами - наиболее очевидным является использование внешних библиотек, которые вы сами не пишете, которые отличаются тем, как они упаковывают данные.

Вещи просто взрываются, когда они сталкиваются. Лучше всего проявлять осторожность в таких случаях и действительно быть близким к проблеме. Если вы не уверены, оставьте его по умолчанию. Если вы не уверены, но должны использовать что-то вроде SSE, где выравнивание - это король (а не по умолчанию или просто длинный снимок), обратитесь к различным ресурсам в Интернете или задайте другой вопрос здесь.

Ответ 2

Я сделаю аналогию, чтобы помочь вам понять.

Предположим, что есть длинная буханка хлеба, и у вас есть машина для резки, которая может разрезать ее на кусочки равной толщины. Тогда вы раздаете эти хлеб, пусть говорят дети. Каждый ребенок берет свой хлеб и справедливо делает то, что они хотят с ними делать (наносите Nutella на них и ешьте и т.д.). Они могут даже сделать из него более тонкие кусочки и использовать его таким образом.

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

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

Ответ 4

char не занимает четыре байта: он берет один байт, как обычно. Вы можете проверить его, распечатав sizeof(char). Остальные три байта заполняют, что компилятор вставляет для оптимизации доступа к другим членам вашего класса. В зависимости от аппаратного обеспечения часто бывает гораздо быстрее обращаться к многобайтовым типам, скажем, к 4-байтовым целям, когда они расположены по адресу, делящемуся на четыре. Компилятор может вставлять до трех байтов заполнения перед тем, как член int выравнивает его с хорошим адресом памяти для более быстрого доступа.

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

cout << offsetof(Storer, x) << endl;
cout << offsetof(Storer, y) << endl;
cout << offsetof(Storer, z) << endl;