內(nèi)存對齊

首先通過一段代碼來描述內(nèi)存對齊的現(xiàn)象。

struct x_ {
    char a;     // 1 byte
    int b;      // 4 bytes
    short c;    // 2 bytes
    char d;     // 1 byte
} MyStruct1;

struct y_ {
    int b;      // 4 bytes
    char a;     // 1 byte
    char d;     // 1 byte
    short c;    // 2 bytes
} MyStruct2;

NSLog(@"%lu,%lu", sizeof(MyStruct1), sizeof(MyStruct2));

上述代碼打印出來的結(jié)果為:12,8

為什么相同的結(jié)構(gòu)體,只是交換了變量 ab 和 cd 在結(jié)構(gòu)體中的順序他們的大小就改變了呢?這就是“內(nèi)存對齊”的現(xiàn)象。

內(nèi)存對齊規(guī)則

在了解為什么要進(jìn)行內(nèi)存對齊之前,先來了解一下內(nèi)存對齊的規(guī)則:

  1. 數(shù)據(jù)成員對齊規(guī)則:struct 或 union (以下統(tǒng)稱結(jié)構(gòu)體)的數(shù)據(jù)成員,第一個(gè)數(shù)據(jù)成員放在偏移為 0 的地方,以后每個(gè)數(shù)據(jù)成員的偏移為 #pragma pack 指定的數(shù)值和這個(gè)數(shù)據(jù)成員自身長度中較小那個(gè)的整數(shù)倍。

  2. 數(shù)據(jù)成員為結(jié)構(gòu)體:如果結(jié)構(gòu)體的數(shù)據(jù)成員還為結(jié)構(gòu)體,則該數(shù)據(jù)成員的“自身長度”為其內(nèi)部最大元素的大小。(struct a 里存有 struct b,b 里有char,int,double等元素,那 b “自身長度”為 8)

  3. 結(jié)構(gòu)體的整體對齊規(guī)則:在數(shù)據(jù)成員按照 #1 完成各自對齊之后,結(jié)構(gòu)體本身也要進(jìn)行對齊。對齊會(huì)將結(jié)構(gòu)體的大小增加為 #pragma pack 指定的數(shù)值和結(jié)構(gòu)體最大數(shù)據(jù)成員長度中較小那個(gè)的整數(shù)倍。

#pragma pack (n) 表示設(shè)置為 n 字節(jié)對齊。 Xcode 默認(rèn)為 8 字節(jié)對齊。當(dāng)設(shè)置為 #pragma pack (1) 時(shí)就代表不進(jìn)行內(nèi)存對齊,上述代碼打印的結(jié)果就都為 8。

MyStruct1 的進(jìn)行對齊后結(jié)構(gòu)為:

// Shows the actual memory layout
struct x_ {
   char a;              // 1 byte
   char _pad0[3];       // padding to put 'b' on 4-byte boundary
   int b;               // 4 bytes
   short c;             // 2 bytes
   char d;              // 1 byte
   char _pad1[1];       // padding to make sizeof(x_) multiple of 4
}

為了進(jìn)行驗(yàn)證,我們通過如下代碼打印結(jié)構(gòu)體:

long a = (long)&MyStruct1.a;
long b = (long)&MyStruct1.b;
long c = (long)&MyStruct1.c;
long d = (long)&MyStruct1.d;
NSLog(@"%ld,%ld,%ld,%ld", a, b, c, d);

輸出的結(jié)果為:4296671176,4296671180,4296671184,4296671186。他們的內(nèi)存占用符合內(nèi)存對齊的規(guī)則。

char a + char _pad0[3] :    4296671176 // 占用 6 7 8 9
int b :                     4296671180 // 占用 0 1 2 3
short c :                   4296671184 // 占用 4 5
char d + char _pad1[1] :    4296671186 // 占用 6 7 8 9

通過上述規(guī)則進(jìn)行對齊后的 MyStruct1 增加了 4 個(gè)字節(jié)變?yōu)?12 字節(jié)。而 MyStruct2 的所有數(shù)據(jù)成員和結(jié)構(gòu)體本身都正好符合了內(nèi)存對齊的規(guī)則,所以沒有增加任何大小正好為 8 字節(jié)。

為什么要進(jìn)行內(nèi)存對齊

內(nèi)存對齊應(yīng)該是編譯器的管轄范圍。編譯器會(huì)為程序中的每個(gè)數(shù)據(jù)單元安排在適當(dāng)?shù)奈恢蒙?,這個(gè)過程對于大部分程序員來說都應(yīng)該是透明的。但如果你想了解更加底層的秘密,“內(nèi)存對齊”就不應(yīng)該對你透明了。

要想掌控這項(xiàng)技術(shù),在了解內(nèi)存對齊的規(guī)則后,還應(yīng)該知道編譯器為什么會(huì)進(jìn)行內(nèi)存對齊。

很多 CPU(如基于 Alpha,IA-64,MIPS,和 SuperH 體系的)拒絕讀取未對齊數(shù)據(jù)。當(dāng)一個(gè)程序要求這些 CPU 讀取未對齊數(shù)據(jù)時(shí),這時(shí) CPU 會(huì)進(jìn)入異常處理狀態(tài)并且通知程序不能繼續(xù)執(zhí)行。舉個(gè)例子,在 ARM,MIPS,和 SH 硬件平臺(tái)上,當(dāng)操作系統(tǒng)被要求存取一個(gè)未對齊數(shù)據(jù)時(shí)會(huì)默認(rèn)給應(yīng)用程序拋出硬件異常。所以,如果編譯器不進(jìn)行內(nèi)存對齊,那在很多平臺(tái)的上的開發(fā)將難以進(jìn)行。

那么,為什么這些 CPU 會(huì)拒絕讀取未對齊數(shù)據(jù)?是因?yàn)槲磳R的數(shù)據(jù),會(huì)大大降低 CPU 的性能。下邊會(huì)進(jìn)行詳細(xì)的解釋。

CPU 存取原理

程序員通常認(rèn)為內(nèi)存就像所有字節(jié)堆起來的數(shù)組。

但是,你的 CPU 并不是以字節(jié)為單位存取數(shù)據(jù)的。每次內(nèi)存存取都會(huì)產(chǎn)生一個(gè)固定的開銷,減少內(nèi)存存取次數(shù)將提升程序的性能。所以 CPU 一般會(huì)以 2/4/8/16/32 字節(jié)為單位來進(jìn)行存取操作。我們將上述這些存取單位稱為內(nèi)存存取粒度。

為了說明內(nèi)存對齊背后的原理,我們通過一個(gè)例子來說明從未地址與對齊地址讀取數(shù)據(jù)的差異。這個(gè)例子很簡單:在一個(gè)存取粒度為 4 字節(jié)的內(nèi)存中,先從地址 0 讀取 4 個(gè)字節(jié)到寄存器,然后從地址 1 讀取 4 個(gè)字節(jié)到寄存器。

當(dāng)從地址 0 開始讀取數(shù)據(jù)時(shí),是讀取對齊地址的數(shù)據(jù),直接通過一次讀取就能完成。當(dāng)從地址 1 讀取數(shù)據(jù)時(shí)讀取的是非對齊地址的數(shù)據(jù)。需要讀取兩次數(shù)據(jù)才能完成。

而且在讀取完兩次數(shù)據(jù)后,還要將 0-3 的數(shù)據(jù)向上偏移 1 字節(jié),將 4-7 的數(shù)據(jù)向下偏移 3 字節(jié)。最后再將兩塊數(shù)據(jù)合并放入寄存器。

這對 CPU 的開銷很大。所以有些處理器才不情愿為你做這些工作。

歷史

最初的 68000 處理器的存取粒度是雙字節(jié),沒有應(yīng)對非對齊內(nèi)存地址的電路系統(tǒng)。當(dāng)遇到非對齊內(nèi)存地址的存取時(shí),它將拋出一個(gè)異常。最初的 Mac OS 并沒有妥善處理這個(gè)異常,它會(huì)直接要求用戶重啟機(jī)器。實(shí)在是悲劇。

隨后的 680x0 系列,像 68020,放寬了這個(gè)的限制,支持了非對齊內(nèi)存地址存取的相關(guān)操作。這解釋了為什么一些在 68020 上正常運(yùn)行的舊軟件會(huì)在 68000 上崩潰。這也解釋了為什么當(dāng)時(shí)一些老 Mac 編程人員會(huì)將指針初始化成奇數(shù)地址。在最初的 Mac 機(jī)器上如果指針在使用前沒有被重新賦值成有效地址,Mac 會(huì)立即跳到調(diào)試器。通常他們通過檢查調(diào)用堆棧會(huì)找到問題所在。

所有的處理器都使用有限的晶體管來完成工作。支持非對齊內(nèi)存地址的存取操作會(huì)消減“晶體管預(yù)算”,這些晶體管原本可以用來提升其他模塊的速度或者增加新的功能。

以速度的名義犧牲非對齊內(nèi)存存取功能的一個(gè)例子就是 MIPS。為了提升速度,MIPS 幾乎廢除了所有的瑣碎功能。

PowerPC 各取所長。目前所有的 PowerPC 都在硬件上支持非對齊的 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 處理器缺少對非對齊的 64-bit 浮點(diǎn)型數(shù)據(jù)的存取的硬件支持。當(dāng)被要求從非對齊內(nèi)存讀取浮點(diǎn)數(shù)時(shí),PowerPC 會(huì)拋出異常并讓操作系統(tǒng)在軟件層面處理內(nèi)存對齊。軟件解決內(nèi)存對齊要比硬件慢得多。經(jīng)過 IBM 在 PowerPC 測試,他們效率的差異大概在 4610%。

總結(jié)

在 iOS 開發(fā)中編譯器會(huì)幫我們進(jìn)行內(nèi)存對齊。所以這些問題都無需考慮。但如果編譯器沒有提供這些功能,而且 CPU 也不支持讀取非對齊數(shù)據(jù),CPU 就會(huì)拋出硬件異常交給操作系統(tǒng)處理,從而產(chǎn)生 4610% 的差異。如果 CPU 支持讀取非對齊數(shù)據(jù),相比對齊數(shù)據(jù),你還是要承擔(dān)額外的開銷造成的損失。誠然,這種損失絕不會(huì)像 4610% 那么大,但還是不能忽略的。

了解了這些后,當(dāng)我們再聲明結(jié)構(gòu)體時(shí)就應(yīng)該合理的安排內(nèi)部數(shù)據(jù)的順序,從而使其占用盡可能小的內(nèi)存。你也許覺得這并沒有什么卵用,但蘋果在 Runloop 的源碼中就使用了 _padding[3] 來手動(dòng)對齊內(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;
    //……
};

博客:xuyafei.cn
簡書:jianshu.com/users/2555924d8c6e
微博:weibo.com/xuyafei86
Github:github.com/xiaofei86

參考資料

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 通過一段代碼來描述內(nèi)存對齊的現(xiàn)象。 上述代碼打印出來的結(jié)果為:24,16 為什么相同的結(jié)構(gòu)體,只是交換了變量 ab...
    豆瓣菜閱讀 7,037評論 5 26
  • 內(nèi)存對齊 為什么是16,而不是9呢?這就是因?yàn)閮?nèi)存對齊的原因 為什么需要內(nèi)存對齊 計(jì)算機(jī)系統(tǒng)對基本數(shù)據(jù)類型的合法地...
    wayyyy閱讀 379評論 0 0
  • 先看一個(gè)例子! 我們發(fā)現(xiàn)兩個(gè)結(jié)構(gòu)體中存放的是相同類型的三個(gè)數(shù)據(jù),只是順序有所不同,通過sizeof()函數(shù)計(jì)算出所...
    longjianjiang閱讀 715評論 0 0
  • 從底層來說,計(jì)算機(jī)訪問內(nèi)存的兩種方式: 從為word_size(比如32位或者64位)的倍數(shù)的地址開始, 1. 讀...
    劉煌旭閱讀 2,910評論 4 4
  • 我知道我沒有資本 提出這樣的要求 我死之后 把我埋在雪山之巔 也許那里才是最干凈 最理想的人生境界 沒有嘈雜與煩惱...
    覓緣人閱讀 428評論 0 0

友情鏈接更多精彩內(nèi)容