iOS 內(nèi)存字節(jié)對(duì)齊

通過(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é)組成。

DataAndAddress1

但是,你的 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)存存取粒度。

DataAndAddress2

為了說(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ù)才能完成。

BytesAddress

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

BytesAddress1

對(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% 那么大,但還是不能忽略的。

Alignment

了解了這些后,當(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值。

參考資料

5分鐘搞定內(nèi)存字節(jié)對(duì)齊

內(nèi)存對(duì)齊的規(guī)則以及作用

[百度百科] 內(nèi)存對(duì)齊

[Wikipedia] Data structure alignment

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

推薦閱讀更多精彩內(nèi)容

  • 首先通過(guò)一段代碼來(lái)描述內(nèi)存對(duì)齊的現(xiàn)象。 上述代碼打印出來(lái)的結(jié)果為:12,8 為什么相同的結(jié)構(gòu)體,只是交換了變量 a...
    xuyafei86閱讀 3,006評(píng)論 2 15
  • 4.3:新增class的相關(guān)內(nèi)容 今天看到一個(gè)題目: 最開始簡(jiǎn)單的理解為,每個(gè)數(shù)據(jù)的size之和就是偏移量。因?yàn)槠?..
    AwesomeAshe閱讀 747評(píng)論 0 0
  • 轉(zhuǎn)載 結(jié)構(gòu)體對(duì)齊詳解 結(jié)構(gòu)體數(shù)據(jù)成員對(duì)齊的意義 許多實(shí)際的計(jì)算機(jī)系統(tǒng)對(duì)基本類型數(shù)據(jù)在內(nèi)存中存放的位置有限制,它們會(huì)...
    erU閱讀 482評(píng)論 0 3
  • iOS面試小貼士 ———————————————回答好下面的足夠了------------------------...
    不言不愛閱讀 1,998評(píng)論 0 7
  • __block和__weak修飾符的區(qū)別其實(shí)是挺明顯的:1.__block不管是ARC還是MRC模式下都可以使用,...
    LZM輪回閱讀 3,352評(píng)論 0 6