[英]Why is the “alignment” the same on 32-bit and 64-bit systems?
我想知道編譯器是否會在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;
};
那么我可以知道這背后的原因是什么?
填充不是由字大小決定的,而是由每種數據類型的對齊決定的。
在大多數情況下,對齊要求等於類型的大小。 因此對於像int64
這樣的64位類型,您將獲得8字節(64位)對齊。 需要將填充插入到結構中以確保該類型的存儲最終位於正確對齊的地址。
當使用在兩種體系結構上具有不同大小的內置數據類型時,您可能會看到32位和64位之間填充的差異 ,例如指針類型( int*
)。
這是結構成員的填充和對齊中指定的數據類型的對齊要求的問題
每個數據對象都有一個對齊要求。 除結構,聯合和數組之外的所有數據的對齊要求是對象的大小或當前打包大小 (使用
/Zp
或pack pragma指定,以較小者為准)。
並且/ Zp(結構成員對齊)中指定了結構成員對齊的默認值
可用的包裝值如下表所述:
/
Zp
參數效果
1個包含1字節邊界的結構。 與/ Zp相同。
2在2字節邊界上打包結構。
4個字節邊界上的4個結構。
8個包含8字節邊界的結構(x86,ARM和ARM64的默認設置)。
16個包含16字節邊界的結構(x64的默認值)。
由於x86的默認值為/ Zp8,即8字節,因此輸出為16。
但是,您可以使用/Zp
選項指定不同的包裝尺寸。
這是一個帶/Zp4
的現場演示 ,輸出為12而不是16。
每種基本類型的大小和alignof()
(該類型的任何對象必須具有的最小對齊)是與體系結構的寄存器寬度分開的ABI 1設計選擇。
結構包裝規則也可能比僅將每個結構成員對齊到結構內部的最小對齊更復雜; 這是ABI的另一部分。
針對32位x86的MSVC為__int64
提供了4的最小對齊,但其默認的struct-packing規則將結構中的類型與結構的開頭的min(8, sizeof(T))
對齊。 (僅適用於非聚合類型)。 這不是一個直接的引用,這是我對MSVC文檔鏈接的解釋,來自@PW的答案,基於MSVC實際上做的事情。 (我懷疑文本中的“以較小者為准”應該是在parens之外,但也許他們對pragma和命令行選項上的交互有不同的看法?)
(包含char[8]
的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
確實在32位模式下為您提供8字節對齊的內存,因此這對動態分配的對象(這是常見的)很有意義 。 強制堆棧上的本地對象完全對齊會增加對齊堆棧指針的成本,但通過設置struct layout以利用8字節對齊的存儲,我們可以獲得靜態和動態存儲的優勢。
這也可能旨在獲得32位和64位代碼,以便就共享內存的某些結構布局達成一致。 (但請注意,x86-64的默認值為min(16, sizeof(T))
,因此如果有任何16字節類型不是聚合,它們仍然不完全同意struct layout(struct / union / array)並且沒有alignas
。)
4的最小絕對對齊來自32位代碼可以假設的4字節堆棧對齊。 在靜態存儲中,編譯器將為結構外部的變量選擇自然對齊,最多可能為8或16個字節,以便使用SSE2向量進行有效復制。
在較大的函數中,出於性能原因,MSVC可以決定將堆棧對齊8,例如,堆棧上的double
變量實際上可以用單個指令操作,或者也可以用於具有SSE2向量的int64_t
。 請參閱2006年文章中的“ 堆棧對齊”部分: IPF,x86和x64上的Windows數據對齊 。 因此,在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字節對齊結束tmp
幸運。
其他設計也是可能的,例如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時,奔騰CPU至少還在眼前。 Pentium具有64位寬的數據總線,因此如果它的64位對齊,它的FPU實際上可以在單個高速緩存訪問中加載64位double
。
或者對於fild
/ fistp
,在轉換為/從double
時加載/存儲64位整數。 有趣的事實:自然對齊的訪問最多64位在x86上保證原子,因為奔騰: 為什么在x86 上自然對齊的變量原子上的整數賦值?
腳注1 :ABI還包括一個調用約定,或者在MS Windows的情況下,可以選擇各種調用約定,你可以用__fastcall
等函數屬性聲明,但是像long long
這樣的基本類型的大小和對齊要求是也是編譯器必須同意制作可以相互調用的函數的東西。 (ISO C ++標准僅涉及單個“C ++實現”; ABI標准是“C ++實現”如何使它們彼此兼容。)
請注意,struct-layout規則也是ABI的一部分 :編譯器必須在struct layout上相互一致,以創建傳遞結構或指向結構的指針的兼容二進制文件。 否則sx = 10; foo(&x);
sx = 10; foo(&x);
可能寫入相對於結構基礎的不同偏移量而不是單獨編譯的foo()
(可能在DLL中)期望讀取它。
腳注2 :
GCC也有這個C ++ alignof()
錯誤,直到它被修復為C11 _Alignof()
在2018年為g ++ 8修復了一段時間。 根據標准中的引用查看該錯誤報告,該標准得出結論: alignof(T)
應該真正報告您可以看到的最小保證對齊, 而不是您想要的性能首選對齊。 即使用小於alignof(int64_t)
對齊的int64_t*
是未定義的行為。
(它通常可以在x86上正常工作,但是假設整個int64_t
迭代的矢量化將達到16或32字節的對齊邊界可能會出錯 。請參閱為什么對mmap的內存進行未對齊訪問有時會在AMD64上出現段錯誤?與gcc。)
gcc bug報告討論了i386 System V ABI,它具有與MSVC不同的結構包裝規則:基於最小對齊,不是首選。 但是現代的i386 System V維護了16字節的堆棧對齊,所以它只是內部結構(因為結構包裝規則是ABI的一部分),編譯器創建的int64_t
和double
對象不是自然對齊的。 無論如何,這就是GCC錯誤報告討論結構成員作為特例的原因。
與具有MSVC的32位Windows相反,其中struct-packing規則與alignof(int64_t) == 8
兼容,但堆棧上的locals總是可能未完全對齊,除非您使用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*
作為一個arg必須處理這個潛在的欠對齊指針,而沒有得到任何關於它來自何處的信息。
如果alignof(int64_t)
實際上是 8,那么該函數可以在asm中以錯誤指針的方式編寫。 或者可以用C語言編寫SSE2內在函數,如_mm_load_si128()
,在處理0或1個元素到達對齊邊界后需要16字節對齊。
但是對於MSVC的實際行為,有可能沒有任何int64_t
數組元素被16對齊,因為它們都跨越了8字節的邊界。
順便說一句,我不建議直接使用像__int64
這樣的編譯器特定類型。 您可以使用<cstdint>
int64_t
編寫可移植代碼,也就是<stdint.h>
。
在MSVC中, int64_t
與__int64
類型相同。
在其他平台上,它通常會long
或long long
。 int64_t
保證正好是64位,沒有填充,2是補碼,如果提供的話。 (所有理智的編譯器都是針對正常的CPU .C99和C ++需要long long
才能至少為64位,而在具有8位字節和2的冪的寄存器上, long long
通常正好是64位且可以用作int64_t
。或者如果long
是64位類型,那么<cstdint>
可能會將其用作typedef。)
我假設__int64
和long long
在MSVC中是相同的類型,但MSVC無論如何都不強制執行嚴格別名,因此它們是否完全相同並不重要,只是它們使用相同的表示。
結構的對齊是其最大成員的大小。
這意味着如果結構中有一個8字節(64位)的成員,那么結構將對齊到8個字節。
在您描述的情況下,如果編譯器允許結構對齊到4個字節,則可能會導致一個8字節的成員位於緩存行邊界。
假設我們有一個具有16字節高速緩存行的CPU。 考慮這樣的結構:
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
};
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.