(Не применяйте эти правила без раздумий. См. пункт ESR о локальности кеша для членов, которые вы используете вместе. И в многопоточных программах остерегайтесь ложного совместного использования элементов, написанных разными потоками. Как правило, вам не нужны данные для каждого потока в по этой причине вообще используйте одну структуру, если только вы не делаете это для управления разделением с помощью большого alignas(128)
. Это относится к atomic
и неатомарным переменным; важно то, что потоки пишут в строки кеша независимо от того, как они это делают.)
Правило большого пальца: от большего к меньшему alignof()
. Вы не можете сделать ничего идеального везде, но, безусловно, наиболее распространенным случаем в наши дни является разумная нормальная реализация C++ для обычного 32- или 64-битного процессора. Все примитивные типы имеют размеры степени двойки.
Большинство типов имеют 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, рассмотрите возможность размещения указателей связанного списка и дерева влево/вправо в первую очередь. Поиск указателя по узлам в дереве или связанном списке than-the-base">налагает штрафы, когда начальный адрес структуры находится на странице размером 4 КБ, отличной от той, к которой вы обращаетесь. Ставя их на первое место, вы гарантируете, что этого не может быть.
затем 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-байтовые пары строк, отдельно от детектора шаблонов предварительной выборки HW основного стримера L2 и предварительной выборки L1d).
Забавный факт: Rust позволяет компилятору переупорядочивать структуры для лучшей упаковки или по другим причинам. IDK, если какие-либо компиляторы действительно это делают. Вероятно, это возможно только при оптимизации всей программы во время компоновки, если вы хотите, чтобы выбор основывался на том, как фактически используется структура. В противном случае раздельно составленные части программы не могли согласоваться по компоновке.
(@alexis опубликовал ответ только со ссылкой на статью ESR, так что спасибо за эту отправную точку.)
person
Peter Cordes
schedule
26.06.2019
struct foo { int a, b; };
) - person David C. Rankin   schedule 26.06.2019double
совпадает сfloat
(4 байта), что может стать неожиданностью для некоторых (особенно если более 7-8 значащих цифр). требуются, скажем, для частотомера...). - person Peter Mortensen   schedule 26.06.2019