(Не применяйте эти правила, не задумываясь. См. Замечание ESR о локальности кэша для членов, которые вы используете вместе. А в многопоточных программах остерегайтесь ложного совместного использования элементов, написанных разными потоками. Как правило, вам не нужны По этой причине потоки данных в единой структуре вообще отсутствуют, если только вы не делаете это для управления разделением с большим alignas(128)
. Это относится к atomic
и неатомарным переменным; важно, чтобы потоки записывали в строки кэша независимо от того, как они это делают.)
Правило большого пальца: от наибольшего к наименьшему alignof()
. Нет ничего, что вы можете сделать идеально, везде, но на сегодняшний день наиболее распространенным случаем в наши дни является нормальная «нормальная» реализация C ++ для обычного 32- или 64-разрядного процессора. Все примитивные типы имеют размеры степени 2.
У большинства типов alignof(T) = sizeof(T)
или alignof(T)
ограничены шириной регистра реализации. Поэтому более крупные типы обычно более выровнены, чем более мелкие.
Правила упаковки структур в большинстве ABI дают членам структуры абсолютное выравнивание alignof(T)
относительно начала структуры, а сама структура наследует наибольшее alignof()
любого из своих членов.
- Сначала всегда ставьте 64-битные элементы (например,
double
, long long
и int64_t
). Конечно, ISO C ++ не фиксирует эти типы в 64 бит / 8 байт, но на практике на всех процессорах вы заботитесь о них. Люди, портирующие ваш код на экзотические процессоры, могут настроить макеты структур для оптимизации при необходимости.
затем указатели и целые числа ширины указателя: size_t
, intptr_t
и ptrdiff_t
(которые могут быть 32- или 64-разрядными). Все они имеют одинаковую ширину в обычных современных реализациях C ++ для процессоров с плоской моделью памяти.
Если вы заботитесь о процессорах x86 и Intel, рассмотрите возможность размещения в первую очередь связанных списков и указателей влево / вправо. Поиск указателей через узлы в дереве или связанном списке имеет штрафы, если начальный адрес структуры находится на странице 4k, отличной от того, к которому вы обращаетесь . В первую очередь они гарантируют, что это не так.
, затем long
(который иногда 32-битный, даже когда указатели 64-битные, в LLP64 ABI, таких как Windows x64). Но он гарантирован, по крайней мере, такой же ширины, как int
.
, затем 32-битный int32_t
, int
, float
, enum
. (При желании можно разделить int32_t
и float
перед int
, если вы заботитесь о возможных 8/16-битных системах, которые все еще дополняют эти типы до 32-битных, или лучше с их естественным выравниванием. Большинство таких систем не имеют более широкие нагрузки (FPU или SIMD), поэтому более широкие типы все равно должны обрабатываться как несколько отдельных блоков).
ISO C ++ позволяет int
иметь ширину 16 бит или произвольно широкую, но на практике это 32-битный тип даже на 64-битных процессорах. Разработчики ABI обнаружили, что программы, предназначенные для работы с 32-битной int
, просто бесполезно расходуют память (и занимают кэш-память), если int
шире. Не делайте предположений, которые могли бы вызвать проблемы с корректностью, но для «портативной производительности» вы просто должны быть правы в обычном случае.
Люди, настраивающие ваш код для экзотических платформ, могут настроить при необходимости. Если определенная структура структуры критична, возможно, прокомментируйте ваши предположения и аргументацию в заголовке.
- затем
short
/ int16_t
- затем
char
/ int8_t
/ bool
- (для нескольких
bool
флагов, особенно если они в основном для чтения или если они все модифицированы вместе, рассмотрите возможность упаковки их с 1-битными битовыми полями.)
(Для целых типов без знака найдите соответствующий тип со знаком в моем списке.)
Массив кратных 8 массив более узких типов может идти раньше, если вы этого хотите. Но если вы не знаете точных размеров типов, вы не можете гарантировать, что int i
+ char buf[4]
заполнит 8-байтовый выровненный слот между двумя double
с. Но это не плохое предположение, так что я бы сделал это в любом случае, если бы была какая-то причина (например, пространственное расположение элементов, к которым осуществляется доступ) для их объединения, а не в конце.
Экзотические типы : x86-64 System V имеет alignof(long double) = 16
, но i386 System V имеет только alignof(long double) = 4
, sizeof(long double) = 12
.Это 80-битный тип x87, который на самом деле составляет 10 байт, но дополняется до 12 или 16, поэтому он кратен его alignof, что делает возможным создание массивов без нарушения гарантии выравнивания.
И в целом он получаетхитрее, когда сами члены структуры являются агрегатами (структура или объединение) с sizeof(x) != alignof(x)
.
Еще один поворот заключается в том, что в некоторых ABI (например, 32-разрядной Windows, если я правильно помню) члены структуры выровненыдо их размера (до 8 байт) относительно начала структуры , хотя alignof(T)
все еще только 4 для double
и int64_t
.
Это для оптимизации дляобщий случай раздельного выделения 8-байтовой выровненной памяти для одной структуры без предоставления выравнивания гарантия .i386 System V также имеет тот же alignof(T) = 4
для большинства примитивных типов (но malloc
все еще дает вам 8-байтовую выровненную память, потому что alignof(maxalign_t) = 8
).Но в любом случае, i386 System V не имеет этого правила упаковки структуры, поэтому (если вы не упорядочите свою структуру от самой большой до самой маленькой), вы можете получить 8-байтовые члены, выровненные относительно начала структуры..
Большинство процессоров имеют режимы адресации, которые, при наличии указателя в регистре, разрешают доступ к любому байтовому смещению.Максимальное смещение обычно очень велико, но на x86 он сохраняет размер кода, если смещение в байтах соответствует байту со знаком ([-128 .. +127]
).Поэтому, если у вас есть большой массив любого вида, предпочтите поместить его позже в структуру после часто используемых членов.Даже если это будет стоить небольшого дополнения.
Ваш компилятор почти всегда будет создавать код, имеющий адрес структуры в регистре, а не какой-либо адрес в середине структуры, чтобы воспользоваться преимуществами коротких отрицательных смещений.
Эрик С. Рэймонд написал статью Потерянное искусство упаковки конструкций .В частности, раздел Переупорядочение структуры в основном является ответом на этот вопрос.
Он также делает еще один важный момент:
9.Читаемость и локальность кэша
Хотя переупорядочение по размеру - это самый простой способ устранить выпадение, это не обязательно правильно .Есть еще две проблемы: удобочитаемость и локальность кэша.
В большой структуре, которую можно легко разбить по границе строки кэша, имеет смысл поместить 2 вещи рядомесли они всегда используются вместе.Или даже смежный, чтобы разрешить объединение загрузки / хранения, например, копирование 8 или 16 байтов с одним (неотмеченным) целым числом или загрузку / хранение SIMD вместо отдельной загрузки меньших элементов.
Строки кэша обычно составляют 32 или 64 байта на современномЦП.(На современном x86 всегда 64 байта. И у семейства Sandybridge есть пространственный предварительный выборщик смежных линий в кэше L2, который пытается завершить 128-байтовые пары строк, отдельно от основного детектора шаблонов предварительной выборки H2-стримера и предварительной выборки L1d).
Интересный факт: Rust позволяет компилятору переупорядочивать структуры для лучшей упаковки или по другим причинам.IDK, если какие-либо компиляторы действительно делают это, хотя.Вероятно, это возможно только при оптимизации всей программы во время соединения, если вы хотите, чтобы выбор основывался на том, как на самом деле используется структура.В противном случае отдельно скомпилированные части программы не могли бы согласовать компоновку.
(@ alexis опубликовал ответ только для ссылки со ссылкой на статью ESR, так что спасибо за эту отправную точку.)