Skip to content

Perché l'"allineamento" è lo stesso sui sistemi a 32 e 64 bit?

Questa è la risposta più valida che troverai da dare, ma guardala attentamente e vedi se può essere adattata al tuo progetto.

Soluzione:

Il padding non è determinato dalla dimensione della parola, ma dall'allineamento di ciascun tipo di dati.

Nella maggior parte dei casi, il requisito di allineamento è uguale alla dimensione del tipo. Quindi, per un tipo a 64 bit come int64 si otterrà un allineamento di 8 byte (64 bit). È necessario inserire un padding nella struttura per assicurarsi che la memorizzazione del tipo finisca a un indirizzo correttamente allineato.

Si può notare una differenza di padding tra 32 bit e 64 bit quando si utilizzano tipi di dati incorporati che hanno diversi su entrambe le architetture, ad esempio i tipi di puntatore (int*).

Dimensione e alignof() (allineamento minimo che qualsiasi oggetto di quel tipo deve per ogni tipo primitivo è un ABI 1 scelta progettuale separata dalla larghezza del registro dell'architettura.

Le regole di impacchettamento delle strutture possono anche essere più complicate del semplice allineamento di ogni membro della struct al suo allineamento minimo all'interno della struct; questa è un'altra parte dell'ABI.

MSVC con target x86 a 32 bit fornisce __int64 a minimo allineamento di 4, ma le sue regole predefinite di impacchettamento delle strutture allineano i tipi all'interno delle strutture a min(8, sizeof(T)) rispetto all'inizio della struttura. (solo per i tipi non aggregati). Questo è non una citazione diretta, è la mia parafrasi del link ai documenti MSVC dalla risposta di @P.W, basata su ciò che MSVC sembra effettivamente fare. (Sospetto che il "whichever is less" nel testo debba essere fuori dalle parentesi, ma forse stanno facendo un punto diverso sull'interazione tra il pragma e l'opzione della riga di comando).

(Una struttura a 8 byte contenente un elemento char[8] ottiene comunque un allineamento di un solo byte all'interno di un'altra struct, o una struct che contiene un char[8]. alignas(16) ottiene comunque un allineamento a 16 byte all'interno di un'altra struct).

Si noti che l'ISO C++ non garantisce che i tipi primitivi abbiano alignof(T) == sizeof(T). Si noti anche che la definizione di MSVC di alignof() non corrisponde allo standard ISO C++: MSVC dice alignof(__int64) == 8, ma alcuni __int64 hanno meno di questo allineamento 2.


Quindi, sorprendentemente, otteniamo un padding extra anche se MSVC non si preoccupa sempre di assicurarsi che la struttura stessa abbia un allineamento superiore a 4 byte a meno che non lo si specifichi con alignas() sulla variabile, o su un membro della struct, per implicare tale allineamento per il tipo. (ad esempio, una variabile locale struct Z tmp sullo stack all'interno di una funzione avrà solo un allineamento a 4 byte, perché MSVC non utilizza istruzioni aggiuntive come and esp, -8 per arrotondare il puntatore dello stack a un confine di 8 byte).

Tuttavia, new / malloc fornisce una memoria allineata a 8 byte in modalità a 32 bit, quindi ha molto senso per gli oggetti allocati dinamicamente (che sono comuni).. Forzare i locali sullo stack a essere completamente allineati comporterebbe un costo aggiuntivo per l'allineamento del puntatore dello stack, ma impostando il layout della struct per sfruttare la memoria allineata a 8 byte, si ottiene il vantaggio per la memoria statica e dinamica.


Questo potrebbe anche essere pensato per far sì che il codice a 32 e 64 bit si accordino su alcuni layout di struct per la memoria condivisa. (Ma si noti che l'impostazione predefinita per x86-64 è min(16, sizeof(T)), quindi non sono ancora completamente d'accordo sul layout delle struct se ci sono tipi a 16 byte che non sono aggregati (struct/union/array) e non hanno un alignas.)


L'allineamento assoluto minimo di 4 deriva dall'allineamento dello stack a 4 byte che il codice a 32 bit può assumere. Nella memorizzazione statica, i compilatori scelgono un allineamento naturale fino a 8 o 16 byte per le barre al di fuori delle strutture, per una copia efficiente con i vettori SSE2.

Nelle funzioni più grandi, MSVC può decidere di allineare lo stack di 8 per motivi di prestazioni, ad esempio per le funzioni double vars sullo stack che possono essere manipolate con singole istruzioni, o forse anche per int64_t con i vettori SSE2. Si veda la sezione Allineamento dello stack in questo articolo del 2006: Allineamento dei dati di Windows su IPF, x86 e x64. Quindi nel codice a 32 bit non si può dipendere da un elemento int64_t* o double* siano naturalmente allineati.

(Non sono sicuro che MSVC creerà mai un allineamento ancora più ridotto di int64_t o double da solo. Certamente sì, se si usa #pragma pack 1 o -Zp1ma questo cambia l'ABI. Ma per il resto probabilmente no, a meno che non si ritagli uno spazio per un int64_t da un buffer manualmente e non ci si preoccupi di allinearlo. Ma supponendo che alignof(int64_t) è ancora 8, questo sarebbe un comportamento non definito del C++).

Se si usa alignas(8) int64_t tmpMSVC emette istruzioni extra per and esp, -8. Se non lo si usa, MSVC non fa nulla di speciale, quindi è una fortuna se tmp finisca o meno allineato a 8 byte.


Sono possibili altri design, ad esempio l'ABI del System V i386 (utilizzato nella maggior parte dei sistemi operativi non Windows) ha alignof(long long) = 4 ma sizeof(long long) = 8. Queste scelte

Al di fuori delle strutture (ad esempio le vars globali o le locals sullo stack), i moderni compilatori in modalità 32 bit scelgono di allineare int64_t su un confine di 8 byte per efficienza (in modo che possa essere caricato/copiato con carichi MMX o SSE2 a 64 bit, oppure x87 fild per effettuare la conversione int64_t -> double).

Questo è uno dei motivi per cui le versioni moderne dell'ABI System V di i386 mantengono l'allineamento dello stack a 16 byte: in questo modo sono possibili vettori locali allineati a 8 e 16 byte.


Quando è stata progettata l'ABI di Windows a 32 bit, le CPU Pentium erano almeno all'orizzonte. Il Pentium ha bus di dati larghi 64 bit, quindi la sua FPU è in grado di caricare un'interfaccia a 64 bit. double in un singolo accesso alla cache se è allineato a 64 bit.

Oppure per fild / fistp, caricare/salvare un intero a 64 bit quando si converte in/da double. Fatto curioso: gli accessi naturalmente allineati fino a 64 bit sono garantiti atomici su x86, a partire dal Pentium: Perché l'assegnazione di un intero su una variabile naturalmente allineata è atomica su x86?


Nota 1: Un ABI include anche una convenzione di chiamata, o nel caso di MS Windows, una scelta di varie convenzioni di chiamata che si possono dichiarare con attributi di funzione come __fastcall), ma le dimensioni e i requisiti di allineamento per i tipi primitivi come long long sono anch'essi qualcosa che i compilatori devono concordare per creare funzioni che possano chiamarsi a vicenda. (Lo standard ISO C++ parla solo di una singola "implementazione C++"; gli standard ABI sono il modo in cui le "implementazioni C++" si rendono compatibili tra loro).

Si noti che anche le regole di layout delle strutture fanno parte dell'ABI.: i compilatori devono accordarsi tra loro sul layout delle strutture per creare binari compatibili che passano intorno alle strutture o ai puntatori alle strutture. Altrimenti s.x = 10; foo(&x); potrebbe scrivere su un offset diverso rispetto alla base della struttura rispetto a quello di foo() , compilato separatamente.foo()compilato separatamente (magari in una DLL) si aspettava di leggerlo.


Nota 2:

GCC aveva questo C++ alignof() fino a quando non è stato risolto nel 2018 per g++8, qualche tempo dopo essere stato risolto per C11 _Alignof(). Si veda la segnalazione del bug per una discussione basata sulle citazioni dello standard che conclude che alignof(T) dovrebbe riportare l'allineamento minimo garantito che si può vedere, non l'allineamento preferito che si vuole ottenere per le prestazioni, cioè che usando un int64_t* con meno di alignof(int64_t) è un comportamento non definito.

(Di solito funziona bene su x86, ma la vettorializzazione che presuppone un numero intero di int64_t raggiunga un limite di allineamento di 16 o 32 byte può avere un errore. Vedere Perché l'accesso non allineato alla memoria mmappata a volte dà luogo a segfault su AMD64? per un esempio con gcc).

Il bug report di gcc parla dell'ABI del System V i386, che ha regole di struct-packing diverse da MSVC: basate sull'allineamento minimo, non preferito. Ma il moderno i386 System V mantiene l'allineamento dello stack a 16 byte, quindi è solo all'interno delle strutture (a causa delle regole di impacchettamento delle strutture che fanno parte dell'ABI) che il compilatore crei mai int64_t e double che non siano allineati in modo naturale. Ad ogni modo, questo è il motivo per cui il bug report di GCC parlava dei membri delle struct come caso speciale.

È un po' l'opposto di Windows a 32 bit con MSVC, dove le regole di impacchettamento delle strutture sono compatibili con un alignof(int64_t) == 8 ma i locals sullo stack sono sempre potenzialmente sotto-allineati, a meno che non si usi alignas() per richiedere specificamente l'allineamento.

MSVC a 32 bit ha il bizzarro comportamento per cui alignas(int64_t) int64_t tmp non è uguale a int64_t tmp;ed emette istruzioni extra per allineare lo stack. Questo perché alignas(int64_t) è come alignas(8), che è più allineato del minimo effettivo.

void extfunc(int64_t *);

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

(32 bit) x86 MSVC 19.20 -O2 lo compila in questo modo (su Godbolt, include anche GCC a 32 bit e il test-case 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

Ma senza l'opzione alignas(), o con alignas(4), otteniamo il molto più semplice

_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

Potrebbe essere sufficiente push esp invece di LEA/push; si tratta di una piccola ottimizzazione mancata.

Il passaggio di un puntatore a una funzione non in linea dimostra che non si tratta solo di una violazione locale delle regole. Un'altra funzione che riceve un puntatore int64_t* come argomento deve gestire questo puntatore potenzialmente non allineato, senza aver ottenuto alcuna informazione sulla sua provenienza.

Se alignof(int64_t) era davvero 8, quella funzione potrebbe essere scritta a mano in asm in modo da avere un errore sui puntatori non allineati. Oppure potrebbe essere scritta in C con gli intrinseci SSE2 come _mm_load_si128() che richiedono un allineamento a 16 byte, dopo aver gestito elementi 0 o 1 per raggiungere un limite di allineamento.

Ma con il comportamento attuale di MSVC, è possibile che nessuno degli elementi int64_t è allineato a 16, perché sono tutti coprono un confine di 8 byte.


BTW, non raccomanderei l'uso di tipi specifici del compilatore come __int64 direttamente. Si può scrivere codice portatile usando int64_t da , alias .

In MSVC, int64_t sarà dello stesso tipo di __int64.

Su altre piattaforme, in genere sarà long o long long. int64_t è garantito che sia esattamente di 64 bit senza padding e con complemento a 2, se fornito. (Lo è per tutti i compilatori sani di mente che utilizzano CPU normali. C99 e C++ richiedono long long sia almeno a 64 bit e su macchine con byte a 8 bit e registri che siano una potenza di 2, long long è normalmente esattamente a 64 bit e può essere utilizzato come int64_t. Oppure se long è un tipo a 64 bit, allora potrebbe usarlo come typedef).

Presumo che __int64 e long long siano dello stesso tipo in MSVC, ma MSVC non applica comunque lo strict-aliasing, quindi non importa se sono esattamente dello stesso tipo o meno, basta che usino la stessa rappresentazione.

È una questione di requisiti di allineamento del tipo di dati, come specificato in
Padding e allineamento dei membri della struttura

Ogni oggetto dati ha un requisito di allineamento. Il requisito di allineamento per tutti i dati, eccetto le strutture, le unioni e gli array, è la dimensione dell'oggetto o la dimensione di impacchettamento corrente. (specificata con /Zp o con il pragma pack, a seconda di quale sia il minore).

Il valore predefinito per l'allineamento dei membri della struttura è specificato in /Zp (Struct Member Alignment).

I valori di impacchettamento disponibili sono descritti nella tabella seguente:

/Zp argomento Effetto
1 Impacchetta le strutture su confini di 1 byte. Come /Zp.
2 Impacchetta le strutture su confini di 2 byte.
4 Impacchetta strutture su confini di 4 byte.
8 Impacchetta le strutture su confini di 8 byte (impostazione predefinita per x86, ARM e ARM64).
16 Impacchetta le strutture su confini di 16 byte (default per x64).

Poiché l'impostazione predefinita per x86 è /Zp8, che è di 8 byte, l'output è 16.

Tuttavia, è possibile specificare una dimensione di impacchettamento diversa con /Zp l'opzione.
Ecco una dimostrazione dal vivo con /Zp4 che dà come risultato 12 anziché 16.

Se ritieni che il nostro post sia stato utile, vorremmo che lo condividessi con più anziani in questo modo ci aiuterai ad estendere i nostri contenuti.



Utilizzate il nostro motore di ricerca

Ricerca
Generic filters

Lascia un commento

Il tuo indirizzo email non sarà pubblicato.