web-dev-qa-db-fra.com

Pourquoi "l'alignement" est-il le même sur les systèmes 32 bits et 64 bits?

Je me demandais si le compilateur utiliserait un remplissage différent sur les systèmes 32 bits et 64 bits, j'ai donc écrit le code ci-dessous dans un simple projet de console VS2019 C++:

struct Z
{
    char s;
    __int64 i;
};

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

Ce que j'attendais sur chaque paramètre "Platform":

x86: 12
X64: 16

Résultat actuel:

x86: 16
X64: 16

Étant donné que la taille du mot mémoire sur x86 est de 4 octets, cela signifie qu'il doit stocker les octets de i dans deux mots différents. J'ai donc pensé que le compilateur ferait du remplissage de cette façon:

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

Alors, puis-je savoir quelle est la raison derrière cela?

  1. Pour une compatibilité ascendante avec le système 64 bits?
  2. En raison de la limitation de la prise en charge des nombres 64 bits sur le processeur 32 bits?
18
Shen Yuan

La taille et la alignof() (alignement minimum que tout objet de ce type doit avoir) pour chaque type primitif est un ABI 1 choix de conception distinct de la largeur de registre de l'architecture.

Les règles de regroupement de structures peuvent également être plus compliquées que simplement aligner chaque membre de structure sur son alignement minimum à l'intérieur de la structure; c'est une autre partie de l'ABI.

Le ciblage MSVC x86 32 bits donne à __int64 Un alignement - au minimum de 4, mais ses règles de regroupement de structures par défaut alignent les types dans les structures sur min(8, sizeof(T)) par rapport au début de la structure. (Pour les types non agrégés uniquement). C'est pas une citation directe, c'est ma paraphrase du lien de documentation MSVC de la réponse de @ P.W, basé sur ce que MSVC semble réellement faire. (Je soupçonne que le "le moins important" dans le texte est censé être en dehors des parens, mais peut-être qu'ils font un point différent sur l'interaction sur le pragma et l'option de ligne de commande?)

(Une structure de 8 octets contenant un char[8] Obtient toujours un alignement de 1 octet dans une autre structure, ou une structure contenant un membre alignas(16) obtient toujours un alignement de 16 octets dans une autre structure.)

Notez que ISO C++ ne garantit pas que les types primitifs ont alignof(T) == sizeof(T). Notez également que la définition MSVC de alignof() ne correspond pas à la norme ISO C++: MSVC dit alignof(__int64) == 8, mais certains objets __int64 ont moins que cet alignement2.


Donc, de manière surprenante, nous obtenons un remplissage supplémentaire même si MSVC ne prend pas toujours la peine de s'assurer que la structure elle-même a un alignement de plus de 4 octets , sauf si vous spécifiez cela avec alignas() sur la variable, ou sur un membre struct pour impliquer cela pour le type. (par exemple, un struct Z tmp local sur la pile à l'intérieur d'une fonction n'aura qu'un alignement sur 4 octets, car MSVC n'utilise pas d'instructions supplémentaires comme and esp, -8 pour arrondir le pointeur de la pile à 8 octets) frontière.)

Cependant, new/malloc vous donne une mémoire alignée sur 8 octets en mode 32 bits, donc cela a beaucoup de sens pour l'allocation dynamique objets (qui sont communs) . Forcer les sections locales sur la pile à être complètement alignées augmenterait le coût d'alignement du pointeur de la pile, mais en définissant la disposition de la structure pour tirer parti du stockage aligné sur 8 octets, nous obtenons l'avantage pour le stockage statique et dynamique.


Cela peut également être conçu pour obtenir du code 32 et 64 bits pour convenir de certaines dispositions de structure pour la mémoire partagée. (Mais notez que la valeur par défaut pour x86-64 est min(16, sizeof(T)), donc ils ne sont toujours pas entièrement d'accord sur la disposition des structures s'il existe des types de 16 octets qui ne sont pas des agrégats (struct/union/array) et n'ont pas de alignas.)


L'alignement absolu minimum de 4 provient de l'alignement de pile sur 4 octets que le code 32 bits peut supposer. Dans le stockage statique, les compilateurs choisiront l'alignement naturel vers le haut à peut-être 8 ou 16 octets pour les variables en dehors des structures, pour une copie efficace avec les vecteurs SSE2.

Dans les fonctions plus importantes, MSVC peut décider d'aligner la pile par 8 pour des raisons de performances, par ex. pour double vars sur la pile qui peuvent être manipulés avec des instructions simples, ou peut-être aussi pour int64_t avec des vecteurs SSE2. Voir la section Alignement de la pile dans cet article de 2006: Alignement des données Windows sur IPF, x86 et x64 . Ainsi, dans le code 32 bits, vous ne pouvez pas dépendre d'un int64_t* Ou double* Aligné naturellement.

(Je ne sais pas si MSVC créera jamais des objets int64_t Ou double encore moins alignés par lui-même. Certainement oui si vous utilisez #pragma pack 1 Ou -Zp1 , mais cela change l'ABI. Mais sinon, probablement pas, à moins que vous ne découpiez manuellement un espace pour un int64_t dans un tampon et que vous ne vous souciez pas de l'aligner. Mais en supposant que alignof(int64_t) est toujours 8 , ce serait un comportement non défini en C++.)

Si vous utilisez alignas(8) int64_t tmp, MSVC envoie des instructions supplémentaires à and esp, -8. Si vous ne le faites pas, MSVC ne fait rien de spécial, c'est donc de la chance que tmp finisse aligné sur 8 octets ou non.


D'autres conceptions sont possibles, par exemple l'i386 System V ABI (utilisé sur la plupart des systèmes d'exploitation non Windows) a alignof(long long) = 4 mais sizeof(long long) = 8. Ces choix

En dehors des structures (par exemple, les variables globales ou locales sur la pile), les compilateurs modernes en mode 32 bits choisissent d'aligner int64_t Sur une limite de 8 octets pour plus d'efficacité (afin qu'il puisse être chargé/copié avec MMX ou Charges SSE2 64 bits, ou x87 fild pour faire int64_t -> double conversion).

C'est l'une des raisons pour lesquelles la version moderne du système i386 ABI V conserve l'alignement de la pile sur 16 octets: les var locales 8 octets et 16 octets sont donc possibles.


Lors de la conception de l'ABI Windows 32 bits, les processeurs Pentium étaient au moins à l'horizon. Pentium a des bus de données de 64 bits, donc son FPU peut vraiment charger un double 64 bits dans un seul accès au cache si c'est 64 bits alignés.

Ou pour fild/fistp, chargez/stockez un entier 64 bits lors de la conversion vers/depuis double. Fait amusant: les accès naturellement alignés jusqu'à 64 bits sont garantis atomiques sur x86, depuis Pentium: Pourquoi l'affectation d'entiers sur une variable naturellement alignée atomique sur x86?


Note de bas de page 1 : Un ABI comprend également une convention d'appel, ou dans le cas de MS Windows, un choix de diverses conventions d'appel que vous pouvez déclarer avec des attributs de fonction comme __fastcall ), mais les tailles et les exigences d'alignement pour les types primitifs comme long long sont aussi quelque chose sur lequel les compilateurs doivent s'entendre pour créer des fonctions qui peuvent s'appeler . (La norme ISO C++ ne parle que d'une seule "implémentation C++"; les normes ABI sont la façon dont les "implémentations C++" se rendent compatibles entre elles.)

Notez que les règles de struct-layout font également partie de l'ABI : les compilateurs doivent se mettre d'accord sur la disposition des struct pour créer des binaires compatibles qui transmettent les structs ou pointeurs vers des structures. Sinon, s.x = 10; foo(&x); pourrait écrire dans un décalage différent par rapport à la base de la structure que foo() (peut-être dans une DLL) compilé séparément s'attendait à le lire à.


Note de bas de page 2 :

GCC avait également ce bug C++ alignof(), jusqu'à ce qu'il soit corrigé en 2018 pour g ++ 8 quelque temps après avoir été corrigé pour C11 _Alignof(). Voir ce rapport de bogue pour une discussion basée sur des citations de la norme qui concluent que alignof(T) devrait vraiment rapporter l'alignement minimum garanti que vous pouvez jamais voir, pas l'alignement préféré que vous voulez pour la performance. c'est-à-dire que l'utilisation d'un int64_t* avec un alignement inférieur à alignof(int64_t) est un comportement non défini.

(Cela fonctionnera généralement bien sur x86, mais la vectorisation qui suppose qu'un nombre entier d'itérations int64_t Atteindra une limite d'alignement de 16 ou 32 octets peut être défaillante. Voir Pourquoi l'accès non aligné à mmap'ed mémoire parfois segfault sur AMD64? pour un exemple avec gcc.)

Le rapport de bogue gcc traite du système i386 ABI V, qui a des règles de struct-pack différentes de MSVC: basé sur un alignement minimum, non préféré. Mais le système i386 moderne V conserve l'alignement de la pile sur 16 octets, donc c'est seulement à l'intérieur des structures (en raison des règles de struct-packaging qui font partie de l'ABI) que le compilateur crée jamais int64_t et double objets qui sont moins que naturellement alignés. Quoi qu'il en soit, c'est pourquoi le rapport de bogue de GCC parlait des membres struct comme cas particulier.

Un peu à l'opposé de Windows 32 bits avec MSVC où les règles de struct-packing sont compatibles avec une alignof(int64_t) == 8 mais les sections locales de la pile sont toujours potentiellement sous-alignées, sauf si vous utilisez alignas() pour demander spécifiquement l'alignement.

MSVC 32 bits a le comportement bizarre que alignas(int64_t) int64_t tmp n'est pas identique à int64_t tmp;, Et émet des instructions supplémentaires pour aligner la pile . C'est parce que alignas(int64_t) est comme alignas(8), qui est plus aligné que le minimum réel.

void extfunc(int64_t *);

void foo_align8(void) {
    alignas(int64_t) int64_t tmp;
    extfunc(&tmp);
}

(32 bits) x86 MSVC 19.20 -O2 le compile ainsi (sur Godbolt, comprend également GCC 32 bits et le cas de test 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

Mais sans alignas(), ou avec alignas(4), nous obtenons le plus simple

_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

Il pourrait simplement Push esp Au lieu de LEA/Push; c'est une optimisation manquée mineure.

Passer un pointeur sur une fonction non en ligne prouve qu'il ne s'agit pas seulement de contourner localement les règles. Une autre fonction qui obtient simplement un int64_t* En tant qu'argument doit gérer ce pointeur potentiellement sous-aligné, sans avoir obtenu d'informations sur son origine.

Si alignof(int64_t) était vraiment 8, cette fonction pourrait être écrite à la main en asm d'une manière qui ferait défaut sur les pointeurs mal alignés. Ou il pourrait être écrit en C avec des éléments intrinsèques SSE2 comme _mm_load_si128() qui nécessitent un alignement de 16 octets, après avoir manipulé 0 ou 1 éléments pour atteindre une limite d'alignement.

Mais avec le comportement réel de MSVC, il est possible qu'aucun des éléments du tableau int64_t Ne soit aligné sur 16, car ils tous s'étendent sur une limite de 8 octets.


BTW, je ne recommanderais pas d'utiliser directement des types spécifiques au compilateur comme __int64. Vous pouvez écrire du code portable en utilisant int64_t De <cstdint> , alias <stdint.h>.

Dans MSVC, int64_t Sera du même type que __int64.

Sur d'autres plates-formes, ce sera généralement long ou long long. int64_t Est garanti d'être exactement 64 bits sans remplissage, et le complément de 2, s'il est fourni. (Il s'agit de tous les compilateurs sensés ciblant les processeurs normaux. C99 et C++ nécessitent que long long Soit au moins 64 bits, et sur les machines avec des octets 8 bits et des registres d'une puissance de 2, long long est normalement exactement 64 bits et peut être utilisé comme int64_t. Ou si long est un type 64 bits, alors <cstdint> pourrait l'utiliser comme typedef.)

Je suppose que __int64 Et long long Sont du même type dans MSVC, mais MSVC n'applique de toute façon pas de pseudonyme strict, donc peu importe qu'ils soient exactement du même type ou non, juste qu'ils utilisent la même représentation.

8
Peter Cordes

Le remplissage n'est pas déterminé par la taille du mot, mais par l'alignement de chaque type de données.

Dans la plupart des cas, l'exigence d'alignement est égale à la taille du type. Donc, pour un type 64 bits comme int64 vous obtiendrez un alignement de 8 octets (64 bits). Le remplissage doit être inséré dans la structure pour s'assurer que le stockage du type se retrouve à une adresse correctement alignée.

Vous pouvez voir une différence de remplissage entre 32 bits et 64 bits lorsque vous utilisez des types de données intégrés qui ont différentes tailles sur les deux architectures, par exemple les types de pointeurs ( int*).

12
ComicSansMS

Il s'agit d'une exigence d'alignement du type de données comme spécifié dans Remplissage et alignement des membres de la structure

Chaque objet de données a une exigence d'alignement. La condition d'alignement pour toutes les données à l'exception des structures, des unions et des tableaux est soit la taille de l'objet, soit la taille d'emballage actuelle (spécifiée avec /Zp ou le pack pragma, selon le moindre des deux).

Et la valeur par défaut pour l'alignement des membres de la structure est spécifiée dans / Zp (Struct Member Alignment)

Les valeurs d'emballage disponibles sont décrites dans le tableau suivant:

/ Zp argument Effet
1 Emballe les structures sur des limites de 1 octet. Identique à/Zp.
2 Packs de structures sur des limites de 2 octets.
4 Packs de structures sur des limites de 4 octets.
8 Packs de structures sur des limites de 8 octets (par défaut pour x86, ARM et ARM64).
16 Packs de structures sur des limites de 16 octets (par défaut pour x64).

Comme la valeur par défaut pour x86 est/Zp8 qui est de 8 octets, la sortie est 16.

Cependant, vous pouvez spécifier une taille d'emballage différente avec /Zp option.
Voici un Live Demo avec /Zp4 qui donne la sortie comme 12 au lieu de 16.

9
P.W