Мне было интересно, будет ли компилятор использовать разные отступы в 32-битных и 64-битных системах, поэтому я написал следующий код в простом консольном проекте VS2019 C++:
Поскольку размер слова памяти в x86 составляет 4 байта, это означает, что он должен хранить байты i
в двух разных словах. Поэтому я подумал, что компилятор сделает заполнение таким образом:
Ответ 4
Size и alignof()
(минимальное выравнивание, которое должен иметь любой объект этого типа) для каждого примитивного типа - это выбор проекта ABI 1, отдельный от ширины регистра архитектуры.
Правила упаковки структуры также могут быть более сложными, чем просто выравнивание каждого члена структуры в соответствии с его минимальным выравниванием внутри структуры; что другая часть ABI.
Ориентация MSVC на 32-разрядную __int64
x86 дает __int64
минимальное выравнивание 4, но ее стандартные правила упаковки структуры выравнивают типы в структурах по минимуму min(8, sizeof(T))
относительно начала структуры. (Только для неагрегированных типов). Это не прямая цитата, что моя перефразировка ссылки на документы MSVC из ответа @PW основана на том, что MSVC, кажется, действительно делает. (Я подозреваю, что "в зависимости от того, что меньше" в тексте должно быть за пределами символов "Паренс", но, возможно, они обращают особое внимание на взаимодействие с прагмой и параметром командной строки?)
(8-байтовая структура, содержащая char[8]
все еще получает только 1-байтовое выравнивание внутри другой структуры, или структура, содержащая alignas(16)
все еще получает 16-байтовое выравнивание внутри другой структуры.)
Обратите внимание, что ISO C++ не гарантирует, что примитивные типы имеют alignof(T) == sizeof(T)
. Также обратите внимание, что определение alignof()
MSVC не соответствует стандарту 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/массив) и не имеют 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 alignas(8) int64_t tmp
дополнительные инструкции and esp, -8
. Если вы этого не сделаете, MSVC не делает ничего особенного, так что вам повезло, в конечном итоге tmp
8-байтовым или нет.
Возможны другие конструкции, например, i386 System V ABI (используется в большинстве не-Windows ОС) имеет alignof(long long) = 4
но sizeof(long long) = 8
. Эти выборы
Вне структур (например, глобальные переменные или локальные переменные в стеке) современные компиляторы в 32-битном режиме предпочитают выравнивать int64_t
по 8-байтовой границе для эффективности (чтобы его можно было загружать/копировать с помощью 64-битных загрузок MMX или SSE2). или 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: компиляторы должны договариваться друг с другом о структурном макете для создания совместимых двоичных файлов, которые передают структуры или указатели на структуры. В противном случае sx = 10; foo(&x);
sx = 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 обсуждается i386 System V ABI, который имеет правила структурирования, отличные от MSVC: основанный на минимальном выравнивании, не предпочтительный. Но современная i386 System V поддерживает 16-байтовое выравнивание стека, поэтому только внутри структур (из-за правил упаковки структур, являющихся частью ABI) компилятор всегда создает int64_t
и double
объекты, которые выровнены не так, как обычно. В любом случае, именно поэтому в отчете об ошибках GCC обсуждались члены структуры как особый случай.
В отличие от 32-битной Windows с MSVC, где правила структурирования alignof(int64_t) == 8
совместимы с alignof(int64_t) == 8
но 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 и контрольный пример структуры):
_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*
в качестве аргумента, должна иметь дело с этим потенциально int64_t*
указателем, не получая никакой информации о том, откуда она взялась.
Если alignof(int64_t)
действительно alignof(int64_t)
8, эта функция может быть написана от руки в asm таким образом, что это alignof(int64_t)
указателей. Или это может быть написано на C с внутренними SSE2, такими как _mm_load_si128()
которые требуют 16-байтового выравнивания, после обработки 0 или 1 элементов для достижения границы выравнивания.
Но при реальном поведении MSVC возможно, что ни один из int64_t
массива int64_t
будет выровнен на 16, поскольку все они охватывают 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 все равно не применяет строгий псевдоним, поэтому не имеет значения, являются ли они одним и тем же типом или нет, просто они используют одно и то же представление.