一、结构体内存对齐
1.1 结构体内存对齐三大原则
数据成员对⻬规则
结构体(struct
)或联合体(union
)的数据成员,第一个数据成员放在offset
为0
的地方,以后每个数据成员存储的起始位置要从该成员大小或者成员的子成员大小(只要该成员有子成员,比如说是数组,结构体等)的整数倍开始。(比如int
为4
字节,则要从4
的整数倍地址开始存储。)
min(当前开始的位置m n) m=9 n=4 9 10 11 12
结构体作为成员
如果一个结构里有某些结构体成员,则结构体成员要从其内部最大元素大小的整数倍地址开始存储。
比如struct a
中存有struct b
,b
中char
,int
,double
等元素。那b
应该从8
的整数倍开始存储。
struct structB {
char c;
int i;
double d;
};
struct structA {
char c;//1
int i;//4
struct structB b;//从8开始存储
};
-
补齐
结构体的总大小,也就是sizeof
的结果,必须是其内部最大成员的整数倍。不足的要补⻬。
1.2 基本数据类型内存大小
C | OC | 32位 | 64位 |
---|---|---|---|
bool | BOOL(64位) | 1 | 1 |
signed char | (__signed char)int8_t、BOOL(32位) | 1 | 1 |
unsigned char | Boolean | 1 | 1 |
short | int16_t | 2 | 2 |
unsigned short | unichar | 2 | 2 |
int 、int32_t | NSInteger(32位)、boolean_t(32位) | 4 | 4 |
unsigned int | boolean_t(64位)、NSUInteger(32位) | 4 | 4 |
long | NSInteger(64位) | 4 | 8 |
unsigned long | NSUInteger(64位) | 4 | 8 |
long long | int64_t | 8 | 8 |
float | CGFloat(32位) | 4 | 4 |
double | CGFloat(64位) | 8 | 8 |
1.3 获取内存大小
1.3.1 sizeof(expression-or-type)
sizeof()
是C/C++
中的关键字,它是一个运算符,不是函数。作用是取得一个对象(数据类型或者数据对象)的长度(即占用内存的大小,以byte
为单位,返回size_t
)。基本数据类型(int
、double
等)的大小与系统相关。结构体涉及字节对齐。
示例:
struct Stu {
char c;
int i;
double d;
};
void test() {
//基本数据类型
int age = 18;
size_t sizeAge1 = sizeof(age);
size_t sizeAge2 = sizeof age;
size_t sizeAge3 = sizeof(int);
NSLog(@"age size: %zu, %zu, %zu",sizeAge1,sizeAge2,sizeAge3);
//结构体
struct Stu s;
s.c = 'c';
s.i = 18;
s.d = 180.0;
size_t sizeS1 = sizeof(s);
size_t sizeS2 = sizeof s;
size_t sizeS3 = sizeof(struct Stu);
NSLog(@"s size: %zu, %zu, %zu",sizeS1,sizeS2,sizeS3);
//指针
NSObject *obj = [NSObject alloc];
size_t sizeObj1 = sizeof(obj);
size_t sizeObj2 = sizeof obj;
size_t sizeObj3 = sizeof(NSObject *);
NSLog(@"obj size: %zu, %zu, %zu",sizeObj1,sizeObj2,sizeObj3);
}
输出:
age size: 4, 4, 4
s size: 16, 16, 16
obj size: 8, 8, 8
- 通过类型和实例都可以获取内存大小,这也说明开辟的内存大小在类型确定后就已经确定了。
-
sizeof
是运算符不是函数。3
种语法形式都可以,需要注意的是通过类型获取的方式必须在()
中。
1.3.2 class_getInstanceSize
这个函数是runtime
提供的获取类的实例所占用的内存大小。大小只与成员变量有关。获取的是实际占用的空间(8
字节对齐)。
源码如下:
#ifdef __LP64__
# define WORD_MASK 7UL
#else
# define WORD_MASK 3UL
#endif
size_t class_getInstanceSize(Class cls)
{
if (!cls) return 0;
return cls->alignedInstanceSize();
}
// Class's ivar size rounded up to a pointer-size boundary.
uint32_t alignedInstanceSize() const {
return word_align(unalignedInstanceSize());
}
static inline uint32_t word_align(uint32_t x) {
return (x + WORD_MASK) & ~WORD_MASK;
}
uint32_t unalignedInstanceSize() const {
ASSERT(isRealized());
return data()->ro()->instanceSize;
}
这里就和alloc
中instanceSize
没有命中缓存的逻辑一致了。
1.3.3 malloc_size
malloc_size
就是alloc
中实际开辟的空间。
1.4 结构体对齐案例
1. 有如下结构体Struct1
和Struct2
分别占用多大内存?
struct Struct1 {
double a; // [0,7]
char b; // [8]
int c; // 根据第一准则要从4的倍数开始,所以[12,13,14,15]。跳过9,10,11
short d; //[16,17]
}struct1;
//根据第三准则总大小要是8的倍数,那就要分配24字节。
struct Struct2 {
double a; //[0,7]
int b; //[8,11]
char c; //[12]
short d; //根据准则1跳过13,从14开始 [14,15]
}struct2;
//这里0~15大小本来就为16了,所以不需要补齐了。
验证:
NSLog(@"struct1 size :%zu\nstruct2 size:%zu",sizeof(struct1),sizeof(struct2));
输出:
struct1 size :24
struct2 size:16
- 结构体中数据类型顺序不一致占用的内存大小可能不一致。
- 大小计算从
0
开始,Struct2
并没有进行第三原则补齐。
2. 增加一个Struct3
中有结构体嵌套,那么占用大小是多少?
struct Struct3 {
double a; //[0,7]
int b; //[8,11]
char c; //[12]
short d; //跳过13 [14,15]
int e; // [16,19]
struct Struct1 str; //根据准则2,Struct1最大元素为`double`类型,所以从24开始。根据`Struct1`分配的时候24个字节,所以str为[24,47]
}struct3;
//所以Struct3占用内存大小为48字节。
验证:
NSLog(@"struct3 size :%zu",sizeof(struct3));
struct3 size :48
在这里可能有个疑问准则3
是先作用在Struct1
再作用在Struct3
还是最后直接作用在Struct3
?不防验证一下:
struct Struct4 {
struct Struct1 str;
char c;
}struct4;
Struct1
本身占用18
字节,补齐后占用24
字节。如果Struct4
最终占用32
字节那么就是第一种情况,占用24
字节则是第二种情况。
NSLog(@"struct4 size :%zu",sizeof(struct4));
struct4 size :32
这也就验证了猜想,结构体嵌套从内部开始补齐。这也符合常理。
3. 修改结构体如下:
struct S1 {
int a; // 4 [0,3]
char b;// 1 [4]
short c; // 2 [6,7]
}; // 0~7 8字节
struct S2 {
double a; // 8 [0,7]
char b; // 1 [8]
struct S1 s1; // 8 [12,19] 按s1自身中存的最大a的4字节的倍数对齐
bool c; // 1 [20]
};
//0~20一共21个字节,按最大的8字节对齐。应该24字节。
struct S2 s2;
NSLog(@"size :%zu",sizeof(s2));
这个时候s2
大小为多少?
分析:
- S1:
-
int a
占4
个字节,从[0~3]
。 -
char b
占1
个字节,[4]
。 -
short c
占2
个字节,需要以2
字节对齐,所以跳过5
[6~7]
-
S1
整体从0~7
不需要补齐。占8
字节。
-
- S2:
-
double a
占8
字节,[0~7]
。 -
char b
占1
字节,[8]
。 -
struct S1 s1
占8
字节。由于S1
内部最大元素为int a
所以需要4
倍对齐,所以[12~19]
。 -
bool c
占1
字节,[20]
。 -
S2
整体从0~21
一共21
字节,需要按S2
中最大元素double a
补齐。所以应该是24
字节。
-
输出:
size :24
1.5 对齐原理分析
为什么要根据数据类型跳过部分内存呢?跳过的部分为什么不能存储数据?
对于不优化联系存储的情况,CPU
读取8~15
的内存数据,需要先读取1
字节再读取4
字节,cpu
对于要读取的数据大小是有变化的。而优化后cpu
先读取4
字节(由于白色3
字节空白所以可以直接读取4
字节)再读取4
字节在这段内存中是没有变化的。相比于第一种优化后cpu
的要进行的操作少了,这就实现了通过空间换取时间。
二、系统内存开辟
2.1 案例
HPObject
定义如下:
@interface HPObject : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *nickName;
@property (nonatomic, assign) int age;
@property (nonatomic, assign) long height;
@end
调用:
#import "HPObject.h"
#import <objc/runtime.h>
#import <malloc/malloc.h>
HPObject *hpObj = [HPObject alloc];
hpObj.name = @"HotpotCat";
hpObj.age = 18;
NSLog(@"sizeof:%zu class_getInstanceSize:%zu malloc:%zu",sizeof(hpObj),class_getInstanceSize([HPObject class]),malloc_size((__bridge const void *)(hpObj)));
那么sizeof
、class_getInstanceSize
、malloc_size
分别输出多少呢?
验证:
sizeof:8 class_getInstanceSize:40 malloc:48
-
hpObj
是一个结构体指针sizeof
返回8
。 -
class_getInstanceSize
由于存在isa
和8字节对齐
所以返回40
。 -
malloc_size
为什么返回48
呢?
2.2 malloc_size分析
查看API
发现malloc_size
在usr/include
中:
那么就需要源码查看
calloc
的实现逻辑了。calloc
源码在libmalloc
中。在源码调试中实现如下:
void *p = calloc(1, 40);
NSLog(@"%zu",malloc_size(p));
calloc
void *
calloc(size_t num_items, size_t size)
{
return _malloc_zone_calloc(default_zone, num_items, size, MZ_POSIX);
}
calloc
调用了_malloc_zone_calloc
_malloc_zone_calloc核心实现如下
MALLOC_NOINLINE
static void *
_malloc_zone_calloc(malloc_zone_t *zone, size_t num_items, size_t size,
malloc_zone_options_t mzo)
{
//……
void *ptr;
//……
ptr = zone->calloc(zone, num_items, size);
//……
return ptr;
}
内部调用了zone->calloc
。但是只有calloc
的声明没有实现。这个时候有两种方式去找到调用:
-
control + step into
跟进去看跳转到了哪里。 - 直接
po
/p
zone->calloc
查看(有赋值就有存储值)
(lldb) po zone->calloc
(.dylib`default_zone_calloc at malloc.c:385)
(lldb) p zone->calloc
(void *(*)(_malloc_zone_t *, size_t, size_t)) $0 = 0x00000001002e1bb7 (.dylib`default_zone_calloc at malloc.c:385)
default_zone_calloc
static void *
default_zone_calloc(malloc_zone_t *zone, size_t num_items, size_t size)
{
zone = runtime_default_zone();
return zone->calloc(zone, num_items, size);
}
同理这里的zone->calloc
调用到了nano_calloc
nano_calloc
static void *
nano_calloc(nanozone_t *nanozone, size_t num_items, size_t size)
{
size_t total_bytes;
//NULL逻辑不用看
if (calloc_get_size(num_items, size, 0, &total_bytes)) {
return NULL;
}
if (total_bytes <= NANO_MAX_SIZE) {
//重点逻辑
void *p = _nano_malloc_check_clear(nanozone, total_bytes, 1);
if (p) {
return p;
} else {
/* FALLTHROUGH to helper zone */
}
}
//help逻辑大概出错的时候才需要
malloc_zone_t *zone = (malloc_zone_t *)(nanozone->helper_zone);
return zone->calloc(zone, 1, total_bytes);
}
得到核心逻辑在_nano_malloc_check_clear
中。
_nano_malloc_check_clear
_nano_malloc_check_clear
中计算大小的逻辑是在segregated_size_to_fit
中完成的。
segregated_size_to_fit
#define SHIFT_NANO_QUANTUM 4
#define NANO_REGIME_QUANTA_SIZE (1 << SHIFT_NANO_QUANTUM) // 16
static MALLOC_INLINE size_t
segregated_size_to_fit(nanozone_t *nanozone, size_t size, size_t *pKey)
{
size_t k, slot_bytes;
if (0 == size) {
//当size为0的时候直接返回16
size = NANO_REGIME_QUANTA_SIZE; // Historical behavior
}
//(40 + 16 - 1) >> 4 << 4 16字节对齐
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;
}
16
字节对齐,向上对齐。这也就是在系统的内存堆区中对象的内存是16
字节对齐,成员变量是以8
字节对齐(结构体内部)。对象与对象是16
字节对齐。
2.3 为什么以16字节对齐?
为什么对象不以8
字节对齐?而以16
字节对齐?
假如一个对象内部成员变量都是
8
字节大小。
- 以
8
字节对齐内部没有多余空间,更容易发生访问错误。 - 以
16
字节对齐内部有多余空间,不容易发生访问错误。
对于64
字节的空间:
16 32 48 64
8 16 24 32 40 48 56 64
- 明显以
16
字节对齐访问对象和成员变量碰到一起的概率小了。16
字节对齐4
次,8
字节对齐8
次。 - 任何对象都继承自
NSObject
,但是很少有对象只有一个isa
。所以最小的对象都应该是16
。 - 如果用
32
字节对齐呢?很明显空间浪费太大。
三、getInstanceSize与calloc
getInstanceSize
的逻辑可以查看上一篇文章alloc流程。getInstanceSize
与calloc
中字节对齐关系如下:
-
instanceSize
正常情况下会走fastInstanceSize
分支进行16
字节对齐。(setFastInstanceSize
在_read_images
的时候就完成了进行了8
字节对齐逻辑,但是没有进行最小16
字节兜底)。 -
instanceSize
alignedInstanceSize
分支进行了8
字节对齐并且有最小16
字节兜底。但是没有进行16
字节对齐。 -
calloc
中进行了16
字节对齐和最小16
字节的修正。相当于是对instanceSize
的两个分支的兜底。
成员变量字节对齐是8
字节对齐,对象的内存对齐是16
字节对齐
总结
- 结构体对齐(三个原则)
-
3
个原则- 数据成员对⻬规则:从成员大小或者成员的子成员大小整数倍开始。
- 结构体作为成员:从内部成员最大元素大小的整数倍地址开始存储。
- 补齐:必须是内部最大成员的整数倍,不足的要补⻬。
- 对齐原理:优化
CPU
读取速度,以空间换时间。 - 结构体嵌套补齐从内部开始补齐。
-
- 内存大小获取
-
sizeof
:是运算符,不是函数。获取对象的长度(对象本身)。 -
class_getInstanceSize
:获取类的实例所占用的内存大小。大小只与成员变量有关。 -
malloc_size
:alloc
中实际开辟的空间。
-
-
calloc
16
字节对齐,最小返回16。- 最终分配的内存大小逻辑在
segregated_size_to_fit
中。以16
字节对齐向上取整 - 为什么以
16
字节对齐?减少访问错误。
- 最终分配的内存大小逻辑在