Размер и alignof()
(минимальное выравнивание, которое должен иметь любой объект этого типа должен ) для каждого типа примитива - это выбор дизайна ABI 1 отдельно от ширина регистра архитектуры.
Правила упаковки структур также могут быть более сложными, чем просто выравнивание каждого члена структуры в соответствии с его минимальным выравниванием внутри структуры; это еще одна часть ABI.
MSVC, нацеленный на 32-разрядную версию x86, дает __int64
минимум выравнивание 4, но его правила упаковки структуры по умолчанию выравнивают типы внутри структур по min(8, sizeof(T))
относительно начала структуры . (Только для неагрегированных типов). Это , а не прямая цитата, это мой пересказ ссылки на документы MSVC из ответа @ P.W, основанный на том, что MSVC, кажется, действительно делает. (Я подозреваю, что «в зависимости от того, что меньше» в тексте должно быть за пределами символов «Паренс», но, возможно, они обращают особое внимание на взаимодействие с прагмой и параметром командной строки?)
(8-байтовая структура, содержащая char[8]
, по-прежнему получает только 1-байтовое выравнивание внутри другой структуры, или структура, содержащая член alignas(16)
, по-прежнему получает 16-байтовое выравнивание внутри другой структуры.)
Обратите внимание, что ISO C ++ не гарантирует, что примитивные типы имеют alignof(T) == sizeof(T)
. Также обратите внимание, что определение MSVC alignof()
не соответствует стандарту ISO C ++: MSVC говорит alignof(__int64) == 8
, но некоторые __int64
объекты имеют меньшее, чем это выравнивание 2 .
Удивительно, но мы получаем дополнительное заполнение, даже несмотря на то, что MSVC не всегда заботится о том, чтобы сама структура имела более чем 4-байтовое выравнивание , если только вы не укажете это с помощью alignas()
для переменной или на член структуры, чтобы подразумевать это для типа. (например, локальный struct Z tmp
в стеке внутри функции будет иметь только 4-байтовое выравнивание, поскольку MSVC не использует дополнительные инструкции, такие как and esp, -8
, для округления указателя стека до 8-байтовой границы.)
Тем не менее, new
/ malloc
дает вам 8-байтовую память в 32-битном режиме, так что это имеет большой смысл для динамически размещаемых объектов (которые являются общими), Принудительное выравнивание локальных элементов в стеке увеличило бы стоимость выравнивания указателя стека, но, установив struct layout для использования преимуществ 8-байтового выравниваемого хранилища, мы получаем преимущество для статического и динамического хранения.
Это также может быть разработано для получения 32- и 64-разрядного кода для согласования некоторых структурных макетов для разделяемой памяти. (Но обратите внимание, что по умолчанию для x86-64 установлено значение min(16, sizeof(T))
, поэтому они все еще не полностью согласуются с разметкой структуры, если существуют 16-байтовые типы, которые не являются агрегатами (struct / union / array) и не есть alignas
.)
Минимальное абсолютное выравнивание 4 происходит из выравнивания стека 4 байта, которое может принять 32-битный код. В статическом хранилище компиляторы выбирают естественное выравнивание до 8 или 16 байтов для переменных вне структур, для эффективного копирования с векторами SSE2.
В больших функциях MSVC может решить выровнять стек на 8 по соображениям производительности, например для double
переменных в стеке, которыми фактически можно манипулировать с помощью одной инструкции, или, может быть, также для int64_t
с векторами SSE2 См. Раздел Выравнивание стека в этой статье 2006 года: Выравнивание данных Windows на IPF, x86 и x64 . Так что в 32-битном коде вы не можете зависеть от естественного выравнивания int64_t*
или double*
.
(Я не уверен, что MSVC когда-либо будет создавать даже менее выровненные int64_t
или double
объекты самостоятельно. Конечно, да, если вы используете #pragma pack 1
или -Zp1
, но это меняет ABI. Но в противном случае вероятно, нет, если вы не вырежете пространство для int64_t
из буфера вручную и не потрудитесь выровнять его. Но если предположить, что alignof(int64_t)
равно 8, это будет неопределенным поведением C ++.)
Если вы используете alignas(8) int64_t tmp
, MSVC отправляет дополнительные инструкции на and esp, -8
. Если вы этого не сделаете, MSVC не делает ничего особенного, так что, к счастью, tmp
в конечном итоге выровняется на 8 байт или нет.
Возможны другие конструкции, например, i386 System V ABI (используется в большинстве операционных систем, отличных от Windows) имеет alignof(long long) = 4
, но sizeof(long long) = 8
. Эти выборы
За пределами структур (например, глобальные переменные или локальные переменные в стеке) современные компиляторы в 32-битном режиме предпочитают выравнивать int64_t
по 8-байтовой границе для эффективности (чтобы его можно было загружать / копировать с помощью MMX или SSE2 64-битная загрузка или x87 fild
для выполнения int64_t -> двойного преобразования).
Это одна из причин того, что современная версия i386 System V ABI поддерживает выравнивание стека 16 байтов: так что возможны выровненные локальные переменные с 8 и 16 байтами.
Когда разрабатывался 32-битный Windows ABI, процессоры Pentium были как минимум на горизонте. Pentium имеет 64-битные шины данных, , поэтому его FPU действительно может загружать 64-битные double
в один доступ к кэшу , если , это 64-битное выравнивание.
Или для fild
/ fistp
загрузить / сохранить 64-разрядное целое число при преобразовании в / из double
. Интересный факт: естественно выровненные обращения до 64 бит гарантированно атомарны на x86, поскольку Pentium: Почему целочисленное присваивание для естественно выровненной переменной атомарно на x86?
Сноска 1 : ABI также включает соглашение о вызовах или, в случае MS Windows, выбор различных соглашений о вызовах, которые можно объявить с помощью атрибутов функций, таких как __fastcall
), но размеры и требования выравнивания для примитивных типов, таких как long long
, также должны согласовываться компиляторами для создания функций, которые могут вызывать друг друга. (Стандарт ISO C ++ говорит только об одной «реализации C ++»; стандарты ABI - это то, как «реализации C ++» обеспечивают совместимость друг с другом.)
Обратите внимание, что правила структурного макета также являются частью ABI : компиляторы должны договариваться друг с другом о структурном макете для создания совместимых двоичных файлов, которые передают структуры или указатели на структуры. В противном случае s.x = 10; foo(&x);
может записать смещение, отличное от основания структуры, чем отдельно скомпилированный foo()
(возможно, в DLL) ожидал прочитать его в.
Сноска 2 :
В GCC тоже была эта ошибка C ++ alignof()
, пока она не была исправлена в 2018 году для g ++ 8 через некоторое время после исправления для C11 _Alignof()
. Посмотрите этот отчет об ошибках для обсуждения, основанного на цитатах из стандарта, которые заключают, что alignof(T)
должен действительно сообщать о минимальном гарантированном выравнивании, которое вы когда-либо можете видеть, не о предпочтительном выравнивании, которое вы хотите для производительности. то есть использование int64_t*
с выравниванием меньше alignof(int64_t)
является неопределенным поведением.
(Обычно это нормально работает на x86, но векторизация, предполагающая, что целое число итераций int64_t
достигнет границы выравнивания 16 или 32 байта, может дать сбой. См. Почему не выровненный доступ к памяти mmap ' иногда segfault на AMD64? для примера с gcc.)
В отчете об ошибках gcc обсуждается ABI i386 System V, который имеет другие правила упаковки структур, чем MSVC: основанный на минимальном выравнивании, не предпочтительный. Но современная i386 System V поддерживает 16-байтовое выравнивание стека, поэтому только внутри структур (из-за правил упаковки структуры, являющихся частью ABI), которые компилятор создает int64_t
и double
объектов которые меньше естественного выравнивания. В любом случае, именно поэтому в отчете об ошибках GCC обсуждались члены структуры как особый случай.
В отличие от 32-битной Windows с MSVC, где правила структурирования пакетов совместимы с alignof(int64_t) == 8
, но локальные элементы в стеке всегда потенциально не выровнены, если вы не используете alignas()
, чтобы специально запросить выравнивание.
32-разрядный MSVC ведет себя странно, что alignas(int64_t) int64_t tmp
отличается от int64_t tmp;
, и выдает дополнительные инструкции для выравнивания стека . Это потому, что alignas(int64_t)
похоже на alignas(8)
, что более выровнено, чем фактический минимум.
void extfunc(int64_t *);
void foo_align8(void) {
alignas(int64_t) int64_t tmp;
extfunc(&tmp);
}
(32-разрядная версия) x86 MSVC 19.20 -O2 компилирует его следующим образом ( на Godbolt , также включает 32-разрядную GCC и тестовый пример struct):
_tmp$ = -8 ; size = 8
void foo_align8(void) PROC ; foo_align8, COMDAT
push ebp
mov ebp, esp
and esp, -8 ; fffffff8H align the stack
sub esp, 8 ; and reserve 8 bytes
lea eax, DWORD PTR _tmp$[esp+8] ; get a pointer to those 8 bytes
push eax ; pass the pointer as an arg
call void extfunc(__int64 *) ; extfunc
add esp, 4
mov esp, ebp
pop ebp
ret 0
Но без alignas()
или с alignas(4)
мы получаем намного проще
_tmp$ = -8 ; size = 8
void foo_noalign(void) PROC ; foo_noalign, COMDAT
sub esp, 8 ; reserve 8 bytes
lea eax, DWORD PTR _tmp$[esp+8] ; "calculate" a pointer to it
push eax ; pass the pointer as a function arg
call void extfunc(__int64 *) ; extfunc
add esp, 12 ; 0000000cH
ret 0
Может просто push esp
вместо LEA / push; это незначительная пропущенная оптимизация.
Передача указателя на не встроенную функцию доказывает, что это не просто локальное изменение правил. Некоторая другая функция, которая просто получает int64_t*
в качестве аргумента, должна иметь дело с этим потенциально недооцененным указателем, не получив никакой информации о том, откуда она взялась.
Если бы alignof(int64_t)
было на самом деле 8, эта функция могла бы быть написана от руки в asm, что могло бы привести к сбою в неправильно выровненных указателях. Или это может быть написано в C с внутренними SSE2, такими как _mm_load_si128()
, которые требуют 16-байтового выравнивания, после обработки 0 или 1 элементов для достижения границы выравнивания.
Но при реальном поведении MSVC возможно, что ни один из элементов массива int64_t
не будет выровнен на 16, потому что они all охватывают 8-байтовую границу.
Кстати, я бы не рекомендовал использовать типы, специфичные для компилятора, такие как __int64
напрямую. Вы можете написать переносимый код, используя int64_t
из <cstdint>
, он же <stdint.h>
.
В MSVC int64_t
будет того же типа, что и __int64
.
На других платформах это обычно будет long
или long long
. int64_t
гарантированно будет ровно 64 бита без дополнения и дополнения до 2, если оно вообще предусмотрено. (Это все нормальные компиляторы, нацеленные на нормальные процессоры. C99 и C ++ требуют, чтобы long long
был как минимум 64-битным, а на машинах с 8-битными байтами и регистрами, которые имеют степень 2, long long
обычно точно 64 биты и могут использоваться как int64_t
. Или, если long
является 64-битным типом, то <cstdint>
может использовать это как typedef.)
Я предполагаю, что __int64
и long long
- это один и тот же тип в MSVC, но MSVC в любом случае не применяет строгий псевдоним, поэтому не имеет значения, являются ли они одним и тем же типом или нет, только то, что они используют то же представление.