通過(guò)一段代碼來(lái)描述內(nèi)存對(duì)齊的現(xiàn)象。
struct StructOne {
char a; //1字節(jié)
double b; //8字節(jié)
int c; //4字節(jié)
short d; //2字節(jié)
} MyStruct1;
struct StructTwo {
double b; //8字節(jié)
char a; //1字節(jié)
short d; //2字節(jié)
int c; //4字節(jié)
} MyStruct2;
NSLog(@"%lu---%lu--", sizeof(MyStruct1), sizeof(MyStruct2));
上述代碼打印出來(lái)的結(jié)果為:24,16
為什么相同的結(jié)構(gòu)體,只是交換了變量 ab 在結(jié)構(gòu)體中的順序他們的大小就改變了呢?這就是“內(nèi)存對(duì)齊”的現(xiàn)象。
內(nèi)存對(duì)齊規(guī)則
每個(gè)特定平臺(tái)上的編譯器都有自己的默認(rèn)“對(duì)齊系數(shù)”(也叫對(duì)齊模數(shù))。程序員可以通過(guò)預(yù)編譯命令#pragma pack(n),n=1,2,4,8,16來(lái)改變這一系數(shù),其中的n就是你要指定的“對(duì)齊系數(shù)”。
在了解為什么要進(jìn)行內(nèi)存對(duì)齊之前,先來(lái)看看內(nèi)存對(duì)齊的規(guī)則:
1.數(shù)據(jù)成員對(duì)齊規(guī)則:struct 或 union (以下統(tǒng)稱結(jié)構(gòu)體)的數(shù)據(jù)成員,第一個(gè)數(shù)據(jù)成員A放在偏移為 0 的地方,以后每個(gè)數(shù)據(jù)成員B的偏移為(#pragma pack(指定的數(shù)n) 與 該數(shù)據(jù)成員(也就是 B)的自身長(zhǎng)度中較小那個(gè)數(shù)的整數(shù)倍,不夠整數(shù)倍的補(bǔ)齊。
2.數(shù)據(jù)成員為結(jié)構(gòu)體:如果結(jié)構(gòu)體的數(shù)據(jù)成員還為結(jié)構(gòu)體,則該數(shù)據(jù)成員的“自身長(zhǎng)度”為其內(nèi)部最大元素的大小。(struct a 里存有 struct b,b 里有char,int,double等元素,那 b “自身長(zhǎng)度”為 8)
3.結(jié)構(gòu)體的整體對(duì)齊規(guī)則:在數(shù)據(jù)成員按照上述第一步完成各自對(duì)齊之后,結(jié)構(gòu)體本身也要進(jìn)行對(duì)齊。對(duì)齊會(huì)將結(jié)構(gòu)體的大小調(diào)整為(#pragma pack(指定的數(shù)n) 與 結(jié)構(gòu)體中的最大長(zhǎng)度的數(shù)據(jù)成員中較小那個(gè)的整數(shù)倍,不夠的補(bǔ)齊。
Xcode 中默認(rèn)為#pragma pack(8)。如果在代碼執(zhí)行前加一句#pragma pack(1) 時(shí)就代表不進(jìn)行內(nèi)存對(duì)齊,上述代碼打印的結(jié)果就都為 16。
MyStruct1 的進(jìn)行對(duì)齊后結(jié)構(gòu)為:
// Shows the actual memory layout
struct StructOne {
char a; // 1 byte
char _pad0[7]; //補(bǔ)齊7字節(jié)成為8(隨后跟著的 double 大小)的倍數(shù),原則一
double b; // 8 bytes
int c; // 4 bytes
short d; // 2 byte
char _pad1[2]; // 補(bǔ)齊2字節(jié)讓結(jié)構(gòu)體的大小成為最大成員大小double(8字節(jié))的倍數(shù),原則二
}
為了進(jìn)行驗(yàn)證,我們通過(guò)如下代碼打印結(jié)構(gòu)體:
long a = (long)&MyStruct1.a;
long b = (long)&MyStruct1.b ;
long c = (long)&MyStruct1.c;
long d = (long)&MyStruct1.d;
NSLog(@"MyStruct1大小---%lu------", sizeof(MyStruct1));
NSLog(@“內(nèi)存地址---%ld,%ld,%ld,%ld", a, b, c, d);
輸出的結(jié)果為:MyStruct1大小—--24 ;內(nèi)存地址---4539371424,4539371432,4539371440,4539371444。他們的內(nèi)存占用符合內(nèi)存對(duì)齊的規(guī)則。
char a + char _pad0[7] : 4539371424 // 占用 24-31
double b : 4539371432 // 占用 32-39
int c : 4539371440 // 占用 40-43
short d + char _pad1[2]: 4539371444 // 占用 44-47
通過(guò)上述規(guī)則進(jìn)行對(duì)齊后的 MyStruct1 增加了 9 個(gè)字節(jié)變?yōu)?24 字節(jié)。而 MyStruct2 的所有數(shù)據(jù)成員和結(jié)構(gòu)體本身只有 a 運(yùn)用內(nèi)存對(duì)齊的規(guī)則一增加了一個(gè)字節(jié),所以大小為 16 字節(jié)。
為什么要進(jìn)行內(nèi)存對(duì)齊
內(nèi)存對(duì)齊應(yīng)該是編譯器的管轄范圍。編譯器會(huì)為程序中的每個(gè)數(shù)據(jù)單元安排在適當(dāng)?shù)奈恢蒙希@個(gè)過(guò)程對(duì)于大部分程序員來(lái)說(shuō)都應(yīng)該是透明的。但如果你想了解更加底層的秘密,“內(nèi)存對(duì)齊”就不應(yīng)該對(duì)你透明了。
要想掌控這項(xiàng)技術(shù),在了解內(nèi)存對(duì)齊的規(guī)則后,還應(yīng)該知道編譯器為什么會(huì)進(jìn)行內(nèi)存對(duì)齊。
很多 CPU(如基于 Alpha,IA-64,MIPS,和 SuperH 體系的)拒絕讀取未對(duì)齊數(shù)據(jù)。當(dāng)一個(gè)程序要求這些 CPU 讀取未對(duì)齊數(shù)據(jù)時(shí),這時(shí) CPU 會(huì)進(jìn)入異常處理狀態(tài)并且通知程序不能繼續(xù)執(zhí)行。舉個(gè)例子,在 ARM,MIPS,和 SH 硬件平臺(tái)上,當(dāng)操作系統(tǒng)被要求存取一個(gè)未對(duì)齊數(shù)據(jù)時(shí)會(huì)默認(rèn)給應(yīng)用程序拋出硬件異常。所以,如果編譯器不進(jìn)行內(nèi)存對(duì)齊,那在很多平臺(tái)的上的開發(fā)將難以進(jìn)行。
那么,為什么這些 CPU 會(huì)拒絕讀取未對(duì)齊數(shù)據(jù)?是因?yàn)槲磳?duì)齊的數(shù)據(jù),會(huì)大大降低 CPU 的性能。下邊會(huì)進(jìn)行詳細(xì)的解釋。
CPU 存取原理
程序員通常認(rèn)為內(nèi)存印象,由一個(gè)個(gè)的字節(jié)組成。
但是,你的 CPU 并不是以字節(jié)為單位存取數(shù)據(jù)的。CPU把內(nèi)存當(dāng)成是一塊一塊的,塊的大小可以是2,4,8,16字節(jié)大小,因此CPU在讀取內(nèi)存時(shí)是一塊一塊進(jìn)行讀取的。每次內(nèi)存存取都會(huì)產(chǎn)生一個(gè)固定的開銷,減少內(nèi)存存取次數(shù)將提升程序的性能。所以 CPU 一般會(huì)以 2/4/8/16/32 字節(jié)為單位來(lái)進(jìn)行存取操作。我們將上述這些存取單位也就是塊大小稱為(memory access granularity)內(nèi)存存取粒度。
為了說(shuō)明內(nèi)存對(duì)齊背后的原理,我們通過(guò)一個(gè)例子來(lái)說(shuō)明從未地址與對(duì)齊地址讀取數(shù)據(jù)的差異。這個(gè)例子很簡(jiǎn)單:在一個(gè)存取粒度為 4 字節(jié)的內(nèi)存中,先從地址 0 讀取 4 個(gè)字節(jié)到寄存器,然后從地址 1 讀取 4 個(gè)字節(jié)到寄存器。
當(dāng)從地址 0 開始讀取數(shù)據(jù)時(shí),是讀取對(duì)齊地址的數(shù)據(jù),直接通過(guò)一次讀取就能完成。當(dāng)從地址 1 讀取數(shù)據(jù)時(shí)讀取的是非對(duì)齊地址的數(shù)據(jù)。需要讀取兩次數(shù)據(jù)才能完成。
而且在讀取完兩次數(shù)據(jù)后,還要將 0-3 的數(shù)據(jù)向上偏移 1 字節(jié),將 4-7 的數(shù)據(jù)向下偏移 3 字節(jié)。最后再將兩塊數(shù)據(jù)合并放入寄存器。
對(duì)一個(gè)內(nèi)存未對(duì)齊的數(shù)據(jù)進(jìn)行了這么多額外的操作,這對(duì) CPU 的開銷很大,大大降低了CPU性能。所以有些處理器才不情愿為你做這些工作。
歷史
最初的 68000 處理器的存取粒度是雙字節(jié),沒有應(yīng)對(duì)非對(duì)齊內(nèi)存地址的電路系統(tǒng)。當(dāng)遇到非對(duì)齊內(nèi)存地址的存取時(shí),它將拋出一個(gè)異常。最初的 Mac OS 并沒有妥善處理這個(gè)異常,它會(huì)直接要求用戶重啟機(jī)器。實(shí)在是悲劇。
隨后的 680x0 系列,像 68020,放寬了這個(gè)的限制,支持了非對(duì)齊內(nèi)存地址存取的相關(guān)操作。這解釋了為什么一些在 68020 上正常運(yùn)行的舊軟件會(huì)在 68000 上崩潰。這也解釋了為什么當(dāng)時(shí)一些老 Mac 編程人員會(huì)將指針初始化成奇數(shù)地址。在最初的 Mac 機(jī)器上如果指針在使用前沒有被重新賦值成有效地址,Mac 會(huì)立即跳到調(diào)試器。通常他們通過(guò)檢查調(diào)用堆棧會(huì)找到問題所在。
所有的處理器都使用有限的晶體管來(lái)完成工作。支持非對(duì)齊內(nèi)存地址的存取操作會(huì)消減“晶體管預(yù)算”,這些晶體管原本可以用來(lái)提升其他模塊的速度或者增加新的功能。
以速度的名義犧牲非對(duì)齊內(nèi)存存取功能的一個(gè)例子就是 MIPS。為了提升速度,MIPS 幾乎廢除了所有的瑣碎功能。
PowerPC 各取所長(zhǎng)。目前所有的 PowerPC 都在硬件上支持非對(duì)齊的 32 位整型的存取。雖然犧牲掉了一部分性能,但這些損失在逐漸減少。
Power 是 1991 年,Apple、IBM、Motorola 組成的 AIM 聯(lián)盟所發(fā)展出的微處理器架構(gòu)。PowerPC 是整個(gè) AIM 聯(lián)盟平臺(tái)的一部分,并且是到目前為止唯一的一部分。但蘋果電腦自 2005 年起,將旗下電腦產(chǎn)品轉(zhuǎn)用 Intel CPU。
現(xiàn)今的 PowerPC 處理器缺少對(duì)非對(duì)齊的 64-bit 浮點(diǎn)型數(shù)據(jù)的存取的硬件支持。當(dāng)被要求從非對(duì)齊內(nèi)存讀取浮點(diǎn)數(shù)時(shí),PowerPC 會(huì)拋出異常并讓操作系統(tǒng)在軟件層面處理內(nèi)存對(duì)齊。軟件解決內(nèi)存對(duì)齊要比硬件慢得多。經(jīng)過(guò) IBM 在 PowerPC 測(cè)試,他們效率的差異大概在 4610%。
總結(jié)
在 iOS 開發(fā)中編譯器會(huì)幫我們進(jìn)行內(nèi)存對(duì)齊。所以這些問題都無(wú)需考慮。但如果編譯器沒有提供這些功能,而且 CPU 也不支持讀取非對(duì)齊數(shù)據(jù),CPU 就會(huì)拋出硬件異常交給操作系統(tǒng)處理,從而產(chǎn)生 4610% 的差異。如果 CPU 支持讀取非對(duì)齊數(shù)據(jù),相比對(duì)齊數(shù)據(jù),你還是要承擔(dān)額外的開銷造成的損失。誠(chéng)然,這種損失絕不會(huì)像 4610% 那么大,但還是不能忽略的。
了解了這些后,當(dāng)我們?cè)俾暶鹘Y(jié)構(gòu)體時(shí)就應(yīng)該合理的安排內(nèi)部數(shù)據(jù)的順序,從而使其占用盡可能小的內(nèi)存。你也許覺得這并沒有什么卵用,但蘋果在 Runloop 的源碼中就使用了 _padding[3] 來(lái)手動(dòng)對(duì)齊內(nèi)存。
struct __CFRunLoopMode {
CFRuntimeBase _base;
pthread_mutex_t _lock; /* must have the run loop locked before locking this */
CFStringRef _name;
Boolean _stopped;
char _padding[3];
CFMutableSetRef _sources0;
CFMutableSetRef _sources1;
CFMutableArrayRef _observers;
CFMutableArrayRef _timers;
//……
};
ps:Vc,Vs等編譯器默認(rèn)是#pragma pack(8),所以測(cè)試我們的規(guī)則會(huì)正常;注意gcc默認(rèn)是#pragma pack(4),并且gcc只支持1,2,4對(duì)齊。套用三原則里計(jì)算的對(duì)齊值是不能大于#pragma pack指定的n值。