iOS 內存字節對齊

一、代碼 Demo

struct Struct1 {
    char a;         // 1 字節
    double b;       // 8 字節
    int c;          // 4 字節
    short d;        // 2 字節
} MyStruct1;

struct Struct2 {
    double b;       // 8 字節
    char a;         // 1 字節
    int c;          // 4 字節
    short d;        // 2 字節
} MyStruct2;

struct Struct3 {
    double b;       // 8 字節
    char a;         // 1 字節
    short d;        // 2 字節
    int c;          // 4 字節
} MyStruct3;

struct Struct4 {
    double b;       // 8 字節
    char a;         // 1 字節
    short d;        // 2 字節
    struct Struct3 c;   // 16 字節
} MyStruct4;

- (void)demo
{
    NSLog(@"%zd  %zd", sizeof(MyStruct1), sizeof(MyStruct2));
    NSLog(@"%zd  %zd", sizeof(MyStruct3), sizeof(MyStruct4));
}

24  24  
16  32

可以看到 Struct1、Struct2、Struct3 的成員變量的數據類型都是相同的,僅僅調整了變量定義的順序,內存占用大小就發生了變化。

二、內存對齊規則

2.1 對齊系數

對齊系數,也叫對齊模數。

每個特定平臺上的編譯器都有默認的對齊系數。程序員可以通過預編譯命令 #pragma pack(n),n = 1, 2, 4, 8, 16 來改變這一系數,其中的 n 就是“對齊系數”。

#pragma pack(1)

struct Struct1 {
    char a;         // 1 字節
    double b;       // 8 字節
    int c;          // 4 字節
    short d;        // 2 字節
} MyStruct1;

struct Struct3 {
    double b;       // 8 字節
    char a;         // 1 字節
    short d;        // 2 字節
    int c;          // 4 字節
} MyStruct3;


- (void)demo
{
    NSLog(@"%zd  %zd", sizeof(MyStruct1), sizeof(MyStruct3));
}

15  15

僅修改了對齊系數,輸出結果從 24 16 變成了 15 15

蘋果默認的對齊系數為 8

2.2 對齊規則

struct 或 union 的內存對齊的規則:

  1. 數據成員普通數據類型

    第一個數據成員放在偏移為 0 的地方,以后每個數據成員 M 的偏移為對齊系數 n 與 M 自身長度中較小那個數的整數倍,不夠整數倍的補齊。

    struct Struct1 {
        char a;         // 1 字節
        double b;       // 8 字節
        int c;          // 4 字節
    } MyStruct1;
    
    • a 的地址為 [0],內存共占用 1 個字節;
    • b 的地址為 MIN(n, 自身長度) = MIN(8, 8) = 8,b 的地址為 8*x,在這里 x 取 1 即可,所以 b 的地址為 [8],補齊 a 后面 [1] ~ [7] 的空間,內存共占用 16 個字節;
    • c 的地址為 MIN(n, 自身長度) = MIN(8, 4) = 4,所以 c 的地址為 4*x,在 a,b 放置之后,內存已經占用了 8 + 8 = 16 個,此時下一個地址正好是 4 的整數倍,所以 c 的地址為 [16],內存共占用 20 個字節。
  1. 數據成員為 struct 或 union 類型

    struct 或 union 的數據成員還是 struct 或者 union 類型,則該數據成員 M 的“自身長度”為其內部最大元素的大小。

    struct Struct3 {
        double b;       // 8 字節
        char a;         // 1 字節
        short d;        // 2 字節
        int c;          // 4 字節
    } MyStruct3;
    
    struct Struct4 {
        double b;       // 8 字節
        char a;         // 1 字節
        short d;        // 2 字節
        struct Struct3 c;  // 16 字節
    } MyStruct4;
    
    16  32
    

    根據這個規則,c 中含有 double、char、short、int 數據類型,它的自身長度就為 double(8),MIN(n, 自身長度) = MIN(8, 8) = 8,c 的地址偏移值為 8*x,前面的 b、a、d 已經占了 12 個字節,所以 x = 2,需要填充 4 個字節,最終 MyStruct4 共需要占 32 個字節。

  2. struct 或 union 的整體對齊

    在內部數據成員按照上述第一步完成各自對齊之后,結構體本身也要進行對齊。

    將結構體的大小調整為對齊系數 n 與結構體中的最大長度的數據成員中較小那個的整數倍,不夠的補齊。

    struct Struct1 {
        char a;         // 1 字節
        double b;       // 8 字節
        int c;          // 4 字節
        short d;        // 2 字節
    } MyStruct1;
    

    對齊系數 n = 8,結構體中最大長度的數據成員類型為 double,最大成員長度 = 8,MIN(8, 8) = 8,原本已占用內存大小為 8+8+4+2 = 22,最接近的 8 的倍數值是 24,所以補齊 2 個字節,最終共占用 24 個字節。

    補齊之后:

     struct Struct1 {
        char a;           // 1 字節
        char _pad0[7];    // 填充 7 字節
        double b;         // 8 字節
        int c;            // 4 字節
        short d;          // 2 字節
        char _pad1[2];    // 填充 2 字節,讓結構體的大小成為最大成員大小 double(8字節)的倍數
    }
    

2.3 驗證

使用如下代碼打印結構體:

struct Struct1 {
    char a;         // 1 字節
    double b;       // 8 字節
    int c;          // 4 字節
    short d;        // 2 字節
} MyStruct1;

- (void)demo
{
    NSLog(@"%zd", sizeof(MyStruct1));
    NSLog(@"%ld, %ld, %ld, %ld", (long)&MyStruct1.a, (long)&MyStruct1.b, (long)&MyStruct1.c, (long)&MyStruct1.d);
}

24
4442808120, 4442808128, 4442808136, 4442808140
  • char a 的地址是 20,下一個內存地址為 20 + 1;
  • double b 的地址是 28,從 21 直接跳到了 28,之間的為填充字節,下一個內存地址為 28 + 8;
  • int c 的地址是 36,c 與 b 之間沒有填充字節,下一個內存地址為 36 + 4;
  • short d 的地址是 40,d 與 c 之間沒有填充字節,下一個內存地址為 40 + 2;
  • 此時共占用 40 + 2 - 20 = 22 個字節,而打印出來 MyStruct1 占了 24 個字節,所以最后執行了 struct 的整體對齊,在末尾填充了兩個字節。

三、為什么要進行內存對齊

編譯器會為程序中的每個數據單元安排在適當的位置上,這個過程對于大部分程序員來說是透明的。內存對齊是由編譯器處理。

要想掌控這項技術,在了解內存對齊的規則后,還應該知道編譯器為什么會進行內存對齊。

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

那么,為什么這些 CPU 會拒絕讀取未對齊數據?是因為未對齊的數據,會大大降低 CPU 的性能

四、CPU 存取原理

程序員通常認為內存印象,由一個個的字節組成。

但是,你的 CPU 并不是以字節為單位存取數據的。CPU 把內存當成是一塊一塊的,塊的大小可以是 2,4,8,16 字節大小,因此 CPU 在讀取內存時是一塊一塊進行讀取的。每次內存存取都會產生一個固定的開銷,減少內存存取次數將提升程序的性能。所以 CPU 一般會以 2/4/8/16/32 字節為單位來進行存取操作。我們將上述這些存取單位也就是塊大小稱為(memory access granularity)內存存取粒度。

為了說明內存對齊背后的原理,我們通過一個例子來說明,從未地址與對齊地址讀取數據的差異。

這個例子很簡單:在一個存取粒度為 4 字節的內存中,先從地址 0 讀取 4 個字節到寄存器,然后從地址 1 讀取 4 個字節到寄存器。

當從地址 0 開始讀取數據時,是讀取對齊地址的數據,直接通過一次讀取就能完成。當從地址 1 讀取數據時,讀取的是非對齊地址的數據。需要讀取兩次數據才能完成。

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

對一個內存未對齊的數據進行了這么多額外的操作,這對 CPU 的開銷很大,大大降低了 CPU 性能。所以有些處理器才不情愿為你做這些工作。

五、歷史

最初的 68000 處理器的存取粒度是雙字節,沒有應對非對齊內存地址的電路系統。當遇到非對齊內存地址的存取時,它將拋出一個異常。最初的 Mac OS 并沒有妥善處理這個異常,它會直接要求用戶重啟機器。

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

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

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

PowerPC 各取所長。目前所有的 PowerPC 都在硬件上支持非對齊的 32 位整型的存取。雖然犧牲掉了一部分性能,但這些損失在逐漸減少。

Power 是 1991 年,Apple、IBM、Motorola 組成的 AIM 聯盟所發展出的微處理器架構。PowerPC 是整個 AIM 聯盟平臺的一部分,并且是到目前為止唯一的一部分。但蘋果電腦自 2005 年起,將旗下電腦產品轉用 Intel CPU。

現今的 PowerPC 處理器缺少對非對齊的 64-bit 浮點型數據的存取的硬件支持。當被要求從非對齊內存讀取浮點數時,PowerPC 會拋出異常并讓操作系統在軟件層面處理內存對齊。軟件解決內存對齊要比硬件慢得多。經過 IBM 在 PowerPC 測試,他們效率的差異大概在 4610%。

六、總結

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

了解了這些后,當我們再聲明結構體時就應該合理的安排內部數據的順序,從而使其占用盡可能小的內存。你也許覺得這并沒有什么卵用,但蘋果在 Runloop 的源碼中就使用了 _padding[3] 來手動對齊內存。

注意:Vc,Vs等編譯器默認是#pragma pack(8),gcc 默認是 #pragma pack(4),并且 gcc 只支持 1,2,4 對齊。

七、文章

豆瓣菜 - iOS 內存字節對齊
ma772528138 - iOS 內存字節對齊計算方式

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,443評論 6 532
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,530評論 3 416
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,407評論 0 375
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,981評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,759評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,204評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,263評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,415評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,955評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,782評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,983評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,528評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,222評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,650評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,892評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,675評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,967評論 2 374

推薦閱讀更多精彩內容

  • 通過一段代碼來描述內存對齊的現象。 上述代碼打印出來的結果為:24,16 為什么相同的結構體,只是交換了變量 ab...
    豆瓣菜閱讀 6,771評論 5 26
  • 字節對齊有三原則: 1:數據成員對齊規則:結構(struct)(或聯合(union))的數據成員,第一個數據成員放...
    Mage閱讀 655評論 0 0
  • 首先通過一段代碼來描述內存對齊的現象。 上述代碼打印出來的結果為:12,8 為什么相同的結構體,只是交換了變量 a...
    xuyafei86閱讀 3,005評論 2 15
  • 簡介 在傳統體系的計算機中,我們知道CPU的運算速度是最快的,也是最昂貴的部件。其次是寄存器,加速優化與內存的讀寫...
    ninedreams閱讀 920評論 0 1
  • 源網址[英文] github上有大神翻譯了一篇內存對齊的英文文獻,我復現了一下過程; 發現其中有個地方有出入(st...
    十曰立閱讀 1,200評論 0 3