结构体内存对齐
内存对齐原则
-
结构(struct)(或联合(union))的数据成员,第⼀个数据成员放在offset为0的地⽅,以后每个数据成员存储的起始位置要从该成员⼤⼩或者成员的⼦成员⼤⼩(只要该成员有⼦成员,⽐如说是数组,结构体等)的整数倍开始(⽐如int为4字节,则要从4的整数倍地址开始存储。
-
结构体作为成员:如果⼀个结构⾥有某些结构体成员,则结构体成员要从其内部最⼤元素⼤⼩的整数倍地址开始存储.(struct a⾥存有struct b,b⾥有char,int ,double等元素,那b应该从8的整数倍开始存储.)
-
收尾⼯作:结构体的总⼤⼩,也就是sizeof的结果,.必须是其内部最⼤成员的整数倍.不⾜的要补⻬。
struct WLWStruct1 {
double a; // 8 [0 7]
char b; // 1 [8]
int c; // 4 (9 10 11 [12 13 14 15]
short d; // 2 [16 17] 24
}struct1;
struct WLWStruct2 {
double a; // 8 [0 7]
int b; // 4 [8 9 10 11]
char c; // 1 [12]
short d; // 2 (13 [14 15] 16
}struct2;
复制代码
Struct1
和Struct2
虽然内部成员一样,但是排列顺序不一致,对齐原则会导致占用的内存大小不一样
复杂的结构体
struct WLWStruct3 {
double a; // 8 [0 7]
int b; // 4 [8 9 10 11]
char c; // 1 [12]
short d; // 2 (13 [14 15]
int e; // 4 [16 17 18 19] 24
struct WLWStruct1 str; 24
}struct3; 48
复制代码
结构体嵌套,内部结构体也要符合内存对齐原则, 打印如下
NSLog(@"struct1--%lu \n struct2--%lu \n struct3--%lu",sizeof(struct1),sizeof(struct2),sizeof(struct3));
2021-06-21 18:28:30.185599+0800 001-内存对齐原则[10150:4530680]
struct1--24
struct2--16
struct3--48
复制代码
为什么要对齐
主要是为了提高读取内存的速度,只需按照固定的偏移,不需要考虑下一片内存的大小,其实就是一种以空间换取时间的策略
对象内存的影响因素
我们都知道一个类有成员变量,属性,方法,协议
,当你初始化一个对象,对象的大小具体是哪些影响的呢,在底层alloc
流程里,我们发现了一重要的方法,下面我们来逐步分析
static ALWAYS_INLINE id
_class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone,
int construct_flags = OBJECT_CONSTRUCT_NONE,
bool cxxConstruct = true,
size_t *outAllocatedSize = nil)
{
// cls = 初始化的类:WLWPerson
// extraBytes = 0, 额外的字节
// zone = nil
// construct_flags = OBJECT_CONSTRUCT_CALL_BADALLOC
ASSERT(cls->isRealized());
// Read class's info bits all at once for performance
// 一次读取类的所有信息已提高性能
bool hasCxxCtor = cxxConstruct && cls->hasCxxCtor();
bool hasCxxDtor = cls->hasCxxDtor();
bool fast = cls->canAllocNonpointer();
size_t size;
size = cls->instanceSize(extraBytes);
.....
.....
}
复制代码
ALWAYS_INLINE
ALWAYS_INLINE
对应的宏定义#define ALWAYS_INLINE inline __attribute__((always_inline))
是 强制内联函数,意思就是会将函数内的代码,直接插入到调用的地方,不会会编译器编译成函数调用
ALWAYS_INLINE int a() { }
int b() {
a()
}
复制代码
- 汇编代码不会跳转到a,而是a函数内部直接是b函数的一部分, 这样会更快
size = cls->instanceSize(extraBytes)
inline size_t instanceSize(size_t extraBytes) const {
if (fastpath(cache.hasFastInstanceSize(extraBytes))) {
return cache.fastInstanceSize(extraBytes);
}
size_t size = alignedInstanceSize() + extraBytes;
// CF requires all objects be at least 16 bytes.
if (size < 16) size = 16;
return size;
}
size_t fastInstanceSize(size_t extra) const
{
ASSERT(hasFastInstanceSize(extra));
// __builtin_constant_p() 如果x的值在编译时能确定,那么该函数返回值为1
if (__builtin_constant_p(extra) && extra == 0) {
return _flags & FAST_CACHE_ALLOC_MASK16;
} else {
size_t size = _flags & FAST_CACHE_ALLOC_MASK;
// remove the FAST_CACHE_ALLOC_DELTA16 that was added
// by setFastInstanceSize
return align16(size + extra - FAST_CACHE_ALLOC_DELTA16);
}
}
复制代码
- 我们发现这个函数是用来计算内存大小的,
inline
内联函数 fastpath(cache.hasFastInstanceSize(extraBytes))
告诉我们大概率会走缓存这套机制cache.fastInstanceSize(extraBytes)
_flags & FAST_CACHE_ALLOC_MASK
这个是取缓存的的大小align16(size + extra - FAST_CACHE_ALLOC_DELTA16)
16字节对齐- 算法
(x + size_t(15)) & ~size_t(15)
, 也就是后四位抹零处理,也就是16的倍数
举个例子: 5 + 15 & ~15 = 20 & ~15 换成二进制 20 = 0001 0100 15 = 0000 1111 ~15 = 1111 0000 20 & ~15 = 0001 0100 = 0001 0000 = 16 1111 0000 复制代码
alignedInstanceSize()
// May be unaligned depending on class's ivars. uint32_t unalignedInstanceSize() const { ASSERT(isRealized()); return data()->ro()->instanceSize; } // 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; } define WORD_MASK 7UL 复制代码
- 可以看到有一段注释,大小只依赖成员变量
class's ivars
- 也就是
data()->ro()->instanceSize
的大小,clean memory
- 非缓存走的
(x + 7) & ~7
8字结对齐, 算法与16字节对齐一致, 后三位抹零,也就是8的倍数
- 可以看到有一段注释,大小只依赖成员变量
(size < 16) size = 16
最少16字节
为什么用8字节对齐或16字节对齐
- 为了方便读取,
- 我们都知道系统是64位的,按8的倍数读取,也更安全,可以有效的避免读到别的内存
- 容错处理
- 以空间换时间
对象的大小
@interface WLWPerson : NSObject
{
// isa // 8 默认指针变量
NSString *name; // 8
}
@property (nonatomic, copy) NSString *nickName; // 8
@property (nonatomic, assign) int age; // 4
@property (nonatomic) double height; // 8
@property (nonatomic) char c1; // 1
@property (nonatomic) char c2; // 1
- (void)sayNo; // 方法不占用内存
+ (void)sayYes;
@end
WLWPerson *person = [WLWPerson alloc];
NSLog(@"对象指针的内存大小--%lu",sizeof(person));
NSLog(@"对象实际的内存大小<8字节对齐>--%lu",class_getInstanceSize([person class]));
NSLog(@"系统分配的内存大小<16字节对齐>--%lu",malloc_size((__bridge const void *)(person)));
复制代码
2021-06-22 15:36:59.650785+0800 001-内存对齐原则[13780:4765193] 对象指针的内存大小--8
2021-06-22 15:36:59.651364+0800 001-内存对齐原则[13780:4765193] 对象实际的内存大小<8字节对齐>--40
2021-06-22 15:36:59.651499+0800 001-内存对齐原则[13780:4765193] 系统分配的内存大小<16字节对齐>--48
复制代码
- 系统会对底层的结构体进行优化, 比如会将两个
int
类型存在一起,因为一个int
是4字节,两个正好是8字节,避免浪费
malloc分析探索思路
obj = (id)calloc(1, size)
是用来开辟内存给对象的,但是在runtime源码里我们无法看到实现,所以需要引入malloc
源码来一探究竟,在libmalloc
源码中
int main(int argc, const char * argv[]) {
@autoreleasepool {
// 堆 对象的内存 16字节对齐
// 成员变量 8字节对齐 结构内部
// 对象 对象 16字节
void *p = calloc(1, 40);
NSLog(@"%lu",malloc_size(p));
NSLog(@"Hello, World!");
}
return 0;
}
复制代码
编译打印
- 发现调用的是
default_zone_calloc
- 字节对齐算法
size + 16 - 1 >> 4 << 4
, 也是相当于后四位抹零,是16的整数倍 calloc
开辟内存是16字节对齐
为什么成员变量是内部是8字节对齐,而对象是16字节对齐,
- 可以防止野指针访问,访问到别的对象的内存,造成访问错误
- 比如对象 是 24 字节,按16字节对齐,会有空余,更安全,容错率更高
© 版权声明
文章版权归作者所有,未经允许请勿转载。
THE END