Почему «выравнивание» одинаково в 32-битных и 64-битных системах? - PullRequest
16 голосов
/ 30 апреля 2019

Мне было интересно, будет ли компилятор использовать разные отступы в 32-разрядных и 64-разрядных системах, поэтому я написал следующий код в простом консольном проекте VS2019 C ++:

struct Z
{
    char s;
    __int64 i;
};

int main()
{
    std::cout << sizeof(Z) <<"\n"; 
}

Что я ожидал откаждый параметр «Платформа»:

x86: 12
X64: 16

Фактический результат:

x86: 16
X64: 16

Поскольку размер слова памяти в x86 составляет 4 байта, это означает, что он должен хранить байты iв двух разных словах.Поэтому я подумал, что компилятор сделает заполнение следующим образом:

struct Z
{
    char s;
    char _pad[3];
    __int64 i;
};

Так могу ли я узнать, в чем причина этого?

  1. Для прямой совместимости с 64-битной системой?
  2. Из-за ограничения поддержки 64-битных чисел на 32-битном процессоре?

Ответы [ 4 ]

12 голосов
/ 30 апреля 2019

Заполнение определяется не размером слова, а выравниванием каждого типа данных.

В большинстве случаев требование выравнивания равно размеру шрифта. Таким образом, для 64-битного типа, такого как int64, вы получите 8-байтовое (64-битное) выравнивание. Заполнение необходимо вставить в структуру, чтобы убедиться, что хранилище для типа заканчивается по адресу, который выровнен правильно.

Вы можете увидеть разницу в заполнении между 32-битным и 64-битным при использовании встроенных типов данных, которые имеют разных размеров в обеих архитектурах, например, типы указателей (int*).

8 голосов
/ 30 апреля 2019

Это вопрос соответствия требований к типу данных, указанному в Заполнение и выравнивание элементов структуры

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

И значение по умолчанию для выравнивания элементов конструкции указано в / Zp (Выравнивание элементов структуры)

Доступные значения упаковки описаны в следующей таблице:

/ Zp аргумент Эффект
1 Упакует структуры на 1-байтовых границах. То же, что /Zp.
2 Пакетные структуры на 2-байтовых границах.
4 структуры пакета на 4-байтовых границах.
8 Пакетирует структуры на 8-байтовых границах (по умолчанию для x86, ARM и ARM64).
16 Пакетные структуры на 16-байтовых границах (по умолчанию для x64).

Поскольку для x86 по умолчанию используется значение / Zp8, равное 8 байтам, вывод равен 16.

Однако вы можете указать другой размер упаковки с опцией /Zp.
Вот Live Demo с /Zp4, который дает вывод как 12 вместо 16.

4 голосов
/ 01 мая 2019

Размер и 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 в любом случае не применяет строгий псевдоним, поэтому не имеет значения, являются ли они одним и тем же типом или нет, только то, что они используют то же представление.

0 голосов
/ 30 апреля 2019

Выравнивание структуры - это размер ее наибольшего члена.

Это означает, что если в структуре имеется 8-байтовый (64-битный) член, то структура будет выровнена до 8 байтов.

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


Допустим, у нас есть процессор с 16-байтовой строкой кэша. Рассмотрим такую ​​структуру:

struct Z
{
    char s;      // 1-4 byte
    __int64 i;   // 5-12 byte
    __int64 i2;  // 13-20 byte, need two cache line fetches to read this variable
};
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...