目錄
1:內存對齊的原因
2:內存對齊的規則
3:結構體內存分配演練以及在iOS中對象成員的內存分配探索
一 :內存對齊的原因
計算機內存是以字節(Byte)為單位劃分的,理論上CPU可以訪問任意編號的字節,但實際情況并非想象中的一個一個字節取出拼接的,而是根據自己的字長來獨處數據的。
我們都知道CPU的數據總線寬度決定了CPU對數據的吞吐量,例如:64位CPU一次可以處理64bit也就是8個字節的數據,32位一個道理,每次可以處理4個字節的數據。
以32位的CPU為例,實際尋址的步長為4個字節,也就是只對編號為 4 的倍數的內存尋址,例如 0、4、8、12、1000 等,而不會對編號為 1、3、11、1001 的內存尋址。如下圖所示:
這樣做可以實現最快速的方式尋址且不會遺漏一個字節,也不會重復尋址。
那么對于程序而言,一個變量的數據存儲范圍是在一個尋址步長范圍內的話,這樣一次尋址就可以讀取到變量的值,如果是超出了步長范圍內的數據存儲,就需要讀取兩次尋址再進行數據的拼接,效率明顯降低了。例如一個double類型的數據在內存中占據8個字節,如果地址是8,那么好辦,一次尋址就可以了,如果是20呢,那就需要進行兩次尋址了。這樣就產生了數據對齊的規則,也就是將數據盡量的存儲在一個步長內,避免跨步長的存儲,這就是內存對齊。在32位編譯環境下默認4字節對齊,在64位編譯環境下默認8字節對齊。
二 :內存對齊的規則
每個特定平臺上的編譯器都有自己的默認“對齊系數”(也叫對齊模數)。程序員可以通過預編譯命令#pragma pack(n)。在iOS平臺中默認的對齊系數是8
1、數據成員對齊規則:(Struct 或 union 的數據成員)第一個數據成員放在偏移為0的位置,以后每個成員的偏移為 min(對齊系數,自身長度)的整數倍,不夠整數倍的補齊。
2、數據成員為結構體:該數據成員的自身長度為其最大長度的整數倍開始存儲
3、整體對齊規則:數據成員按照上述規則對齊之后,其本身也要對齊,
對齊原則是min(對其系數,成員最大長度)的整數倍。
三 :結構體內存分配演練以及在iOS中對象成員的內存分配探索
我們用以下三個結構體做為例子去探索下內存對齊的規則:
struct Struct1 {
char a; // 1 + 7
double b; // 8
int c; // 4
short d; // 2 + 2
} MyStruct1;
struct Struct2 {
int b; // 0 7
char a; // 8
int c; // min(9 4) = 4
short d; // 2
// 16 17
} MyStruct2;
struct Struct3 {
double b; // 0 7
int c; // min(9 4) = 4
char a; // 8
short d; // 2
// 16 17
} MyStruct3;
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSLog(@"%lu-%lu-%lu",sizeof(MyStruct1),sizeof(MyStruct2),sizeof(MyStruct3));
}
}
分別對他們求大小,在Struct1中:
長度 對齊 偏移 區間
char a; 1 0 [0]
double b; 8 8 [8, 15]
int c; 4 16 [16, 19]
short d; 2 20 [20, 21]
1、數據成員的對齊按照#pragma pack-8和自身長度中比較小的那個進行
-- char a 的自身長度為 1, min(1,8) = 1, 按 1 對齊
2、第一個數據成員放在offset為0的地方
-- char a 的偏移為 0
3、整體對齊系數 = min((8,max(int,short,char,double)) = 8,
將 21 提升到 8 的倍數,則為 24,所以最終結果為 24 個字節
在Struct2中:
長度 對齊 偏移 區間
double b; 8 0 [0, 7]
char a; 1 8 [ 8 ]
int c; 4 12 [12, 15]
short d; 2 16 [16, 17]
1、數據成員的對齊按照#pragma pack-8和自身長度中比較小的那個進行
-- double b 的自身長度為 8, min(8,8) = 8, 按 8 對齊
2、第一個數據成員放在offset為0的地方
-- double b 的偏移為 0
3、整體對齊系數 = min((8,max(int,short,char,double)) = 8,
將 17 提升到 8 的倍數,則為 24,所以最終結果為 24 個字節
在Struct3中:
長度 對齊 偏移 區間
double b; 8 0 [0, 7]
int c; 4 8 [8, 11]
char a; 1 12 [12]
short d; 2 14 [14, 15]
1、數據成員的對齊按照#pragma pack-8和自身長度中比較小的那個進行
-- double b 的自身長度為 8, min(8,8) = 8, 按 8 對齊
2、第一個數據成員放在offset為0的地方
-- double b 的偏移為 0
3、整體對齊系數 = min((8,max(int,short,char,double)) = 8,
將 15 提升到 8 的倍數,則為 16,所以最終結果為 16 個字節
最后看下輸出結果和我們計算是否一致:
[88992:5534353] 24-24-16
我們可以看到struct2和struct3中相同的數據成員,不同的位置,促成了不同的內存分配結果,其原因就是因為我們的內存對齊規則導致的。
三 :在iOS中類對象的內存分配
我們先看一段代碼:
@interface LGTeacher : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) int age;
@property (nonatomic, assign) long height;
@property (nonatomic, strong) NSString *job;
@property (nonatomic, assign) int sex;
@property (nonatomic) char ch1;
@property (nonatomic) char ch2;
@end
Person *person = [[Person alloc] init];
NSLog(@"%lu - %lu",class_getInstanceSize([person class]),malloc_size((__bridge const void *)(person)));
我們創建了一個person對象,并對他的對象實例所占內存大小和系統為此對象開辟空間大小進行打印,得出結果:
[23426:8161973] 40 - 48
為什么對象本身大小和系統為對象分配空間不一致呢?我們根據alloc實現的底層源碼知道,對象是以8個字節對齊的,內存優化之后得到結果40我們可以理解,但是為什么系統要為我們多開辟8個字節的空間呢?
我們看下malloc的底層源碼實現:
我們跟蹤malloc的調用,最后發現這個函數:
segregated_size_to_fit(nanozone_t *nanozone, size_t size, size_t *pKey)
{
size_t k, slot_bytes;
if (0 == size) {
size = NANO_REGIME_QUANTA_SIZE; // Historical behavior
}
k = (size + NANO_REGIME_QUANTA_SIZE - 1) >> SHIFT_NANO_QUANTUM; // round up and shift for number of quanta
slot_bytes = k << SHIFT_NANO_QUANTUM; // multiply by power of two quanta size
*pKey = k - 1; // Zero-based!
return slot_bytes;
}
1: k = (size + NANO_REGIME_QUANTA_SIZE - 1) >> SHIFT_NANO_QUANTUM;
2: slot_bytes = k << SHIFT_NANO_QUANTUM;
我們重點看下這兩行代碼,第一行我們打印size得到了40(也就是對象的大小),其中這兩個宏定義的值NANO_REGIME_QUANTA_SIZE = 16 , SHIFT_NANO_QUANTUM = 4;也就是 k = (40 + 16 - 1) >> 4; 結果右移了4位。然后第二行代碼又對上個結果左移了4位。看到這個是不是和alloc的的對象對齊算法很類似?右移之后再左移相當于抹去了二進制的最后四位,前面又加了一個(16-1)得到的結果是16的倍數(也就是16字節對齊,最小大小為16字節)。
得出結論:對象內存的申請按照8字節對齊,不滿8字節按照8字節計算;但是實際上malloc實際開辟內存的時候,則是進行了16字節對齊,避免對象之間發生溢出和野指針的問題,所以當對象大小為40時,后面要補8位,最后結果是48